mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
test(app): remove useless frontend tests (#2819)
This commit is contained in:
1 parent
ed9528834d
commit
deac653a9e
341 files changed
-62758
No files matched your search
@@ -1,232 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {commitUnderTranslation, isTranslationDomCrash} from '@app/features/i18n/testing/TranslationCommitHarness';
|
||||
import {
|
||||
chromeFaithfulCompoundPipeline,
|
||||
findTranslationUnsafeTextNodes,
|
||||
TRANSLATION_MARKER,
|
||||
TRANSLATOR_PIPELINE_NAMES,
|
||||
TranslatorPipelines,
|
||||
} from '@app/features/i18n/testing/TranslatorSimulation';
|
||||
import {createElement, Fragment, type ReactNode} from 'react';
|
||||
import {afterEach, describe, expect, it} from 'vitest';
|
||||
|
||||
const badge = (on: boolean): ReactNode => (on ? createElement('b', null, '!') : null);
|
||||
const tail = createElement('i', null, 'x');
|
||||
|
||||
const BareLabel = (): ReactNode => 'Hello';
|
||||
const WrappedLabel = (): ReactNode => createElement('flx-i18n', null, 'Hello');
|
||||
|
||||
const wrap = (text: string): ReactNode => createElement('flx-i18n', null, text);
|
||||
const row = (...children: Array<ReactNode>): ReactNode => createElement('div', null, ...children);
|
||||
|
||||
const visibleText = (html: string): string => html.replace(/<[^>]+>/g, '').replaceAll(TRANSLATION_MARKER, '');
|
||||
|
||||
interface Scenario {
|
||||
name: string;
|
||||
unsafe: (on: boolean) => ReactNode;
|
||||
fixed: (on: boolean) => ReactNode;
|
||||
beforeState: boolean;
|
||||
afterState: boolean;
|
||||
expectedText: string;
|
||||
expectedFirstTag: string;
|
||||
}
|
||||
|
||||
const scenarios: Array<Scenario> = [
|
||||
{
|
||||
name: 'placement before a bare text sibling',
|
||||
unsafe: (on) => row(badge(on), 'Hello', tail),
|
||||
fixed: (on) => row(badge(on), wrap('Hello'), tail),
|
||||
beforeState: false,
|
||||
afterState: true,
|
||||
expectedText: '!Hellox',
|
||||
expectedFirstTag: 'B',
|
||||
},
|
||||
{
|
||||
name: 'deletion of a bare text sibling',
|
||||
unsafe: (on) => row(on ? 'Hello' : null, tail),
|
||||
fixed: (on) => row(on ? wrap('Hello') : null, tail),
|
||||
beforeState: true,
|
||||
afterState: false,
|
||||
expectedText: 'x',
|
||||
expectedFirstTag: 'I',
|
||||
},
|
||||
{
|
||||
name: 'placement between adjacent text nodes',
|
||||
unsafe: (on) => row('A', badge(on), 'B', tail),
|
||||
fixed: (on) => row(wrap('A'), badge(on), wrap('B'), tail),
|
||||
beforeState: false,
|
||||
afterState: true,
|
||||
expectedText: 'A!Bx',
|
||||
expectedFirstTag: 'FLX-I18N',
|
||||
},
|
||||
{
|
||||
name: 'placement before text inside a fragment',
|
||||
unsafe: (on) => row(createElement(Fragment, null, badge(on), 'Hello'), tail),
|
||||
fixed: (on) => row(createElement(Fragment, null, badge(on), wrap('Hello')), tail),
|
||||
beforeState: false,
|
||||
afterState: true,
|
||||
expectedText: '!Hellox',
|
||||
expectedFirstTag: 'B',
|
||||
},
|
||||
{
|
||||
name: 'placement before a component that returns a bare string',
|
||||
unsafe: (on) => row(badge(on), createElement(BareLabel), tail),
|
||||
fixed: (on) => row(badge(on), createElement(WrappedLabel), tail),
|
||||
beforeState: false,
|
||||
afterState: true,
|
||||
expectedText: '!Hellox',
|
||||
expectedFirstTag: 'B',
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
for (const pipelineName of TRANSLATOR_PIPELINE_NAMES) {
|
||||
const pipeline = TranslatorPipelines[pipelineName];
|
||||
describe(`${pipelineName} pipeline`, () => {
|
||||
for (const scenario of scenarios) {
|
||||
it(`throws the production DOM error on ${scenario.name}`, () => {
|
||||
const result = commitUnderTranslation({
|
||||
before: scenario.unsafe(scenario.beforeState),
|
||||
after: scenario.unsafe(scenario.afterState),
|
||||
pipeline,
|
||||
});
|
||||
expect(result.unsafeTextNodesBeforeTranslation).toBeGreaterThan(0);
|
||||
expect(result.htmlAfterTranslation).not.toBe(result.htmlBeforeTranslation);
|
||||
expect(isTranslationDomCrash(result.error)).toBe(true);
|
||||
});
|
||||
|
||||
if (pipelineName === 'chromeCompound') {
|
||||
it(`leaves the compound pipeline no text-and-element parent to rewrite in ${scenario.name}`, () => {
|
||||
const unsafe = commitUnderTranslation({
|
||||
before: scenario.unsafe(scenario.beforeState),
|
||||
after: scenario.unsafe(scenario.beforeState),
|
||||
pipeline,
|
||||
});
|
||||
expect(unsafe.unsafeTextNodesBeforeTranslation).toBeGreaterThan(0);
|
||||
expect(unsafe.htmlAfterTranslation).not.toBe(unsafe.htmlBeforeTranslation);
|
||||
|
||||
const fixed = commitUnderTranslation({
|
||||
before: scenario.fixed(scenario.beforeState),
|
||||
after: scenario.fixed(scenario.afterState),
|
||||
pipeline,
|
||||
});
|
||||
expect(fixed.unsafeTextNodesBeforeTranslation).toBe(0);
|
||||
expect(fixed.htmlAfterTranslation).toBe(fixed.htmlBeforeTranslation);
|
||||
expect(fixed.error).toBeNull();
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
it(`survives ${scenario.name} once the text is a sole child`, () => {
|
||||
let firstTag: string | null = null;
|
||||
let unsafeTextNodes = -1;
|
||||
const result = commitUnderTranslation({
|
||||
before: scenario.fixed(scenario.beforeState),
|
||||
after: scenario.fixed(scenario.afterState),
|
||||
pipeline,
|
||||
inspect: (host) => {
|
||||
firstTag = host.querySelector('div')?.firstElementChild?.tagName ?? null;
|
||||
unsafeTextNodes = findTranslationUnsafeTextNodes(host).length;
|
||||
},
|
||||
});
|
||||
expect(result.htmlAfterTranslation).not.toBe(result.htmlBeforeTranslation);
|
||||
expect(result.htmlAfterTranslation).toContain(TRANSLATION_MARKER);
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.unsafeTextNodesBeforeTranslation).toBe(0);
|
||||
expect(unsafeTextNodes).toBe(0);
|
||||
expect(firstTag).toBe(scenario.expectedFirstTag);
|
||||
expect(visibleText(result.htmlAfterCommit)).toBe(scenario.expectedText);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('faithful compound pipeline', () => {
|
||||
const inlineWrap = (text: string): ReactNode => createElement('span', null, text);
|
||||
const contentsWrap = (text: string): ReactNode => createElement('flx-i18n', {style: {display: 'contents'}}, text);
|
||||
|
||||
it('defeats a bare text sibling', () => {
|
||||
const result = commitUnderTranslation({
|
||||
before: row(badge(false), 'Hello', tail),
|
||||
after: row(badge(true), 'Hello', tail),
|
||||
pipeline: chromeFaithfulCompoundPipeline,
|
||||
});
|
||||
expect(isTranslationDomCrash(result.error)).toBe(true);
|
||||
});
|
||||
|
||||
it('defeats an inline sole-child wrapper', () => {
|
||||
const result = commitUnderTranslation({
|
||||
before: row(badge(false), inlineWrap('Hello'), tail),
|
||||
after: row(badge(true), inlineWrap('Hello'), tail),
|
||||
pipeline: chromeFaithfulCompoundPipeline,
|
||||
});
|
||||
expect(isTranslationDomCrash(result.error)).toBe(true);
|
||||
});
|
||||
|
||||
it('defeats the display:contents wrapper the app ships', () => {
|
||||
const result = commitUnderTranslation({
|
||||
before: row(badge(false), contentsWrap('Hello'), tail),
|
||||
after: row(badge(true), contentsWrap('Hello'), tail),
|
||||
pipeline: chromeFaithfulCompoundPipeline,
|
||||
});
|
||||
expect(isTranslationDomCrash(result.error)).toBe(true);
|
||||
});
|
||||
|
||||
it('defeats a pure element tree that holds no text node of its own', () => {
|
||||
let reassembledChildren = false;
|
||||
const pureElementRow = (on: boolean): ReactNode =>
|
||||
row(badge(on), createElement('i', null, 'a'), createElement('em', null, 'y'));
|
||||
const result = commitUnderTranslation({
|
||||
before: pureElementRow(false),
|
||||
after: pureElementRow(true),
|
||||
pipeline: (host) => {
|
||||
const before = Array.from(host.querySelector('div')?.childNodes ?? []);
|
||||
chromeFaithfulCompoundPipeline(host);
|
||||
const after = Array.from(host.querySelector('div')?.childNodes ?? []);
|
||||
reassembledChildren = before.some((node, index) => node !== after[index]);
|
||||
},
|
||||
});
|
||||
expect(reassembledChildren).toBe(true);
|
||||
expect(result.unsafeTextNodesBeforeTranslation).toBe(0);
|
||||
expect(isTranslationDomCrash(result.error)).toBe(true);
|
||||
});
|
||||
|
||||
it('is survived only by translate="no"', () => {
|
||||
const result = commitUnderTranslation({
|
||||
before: createElement('div', {translate: 'no'}, badge(false), contentsWrap('Hello'), tail),
|
||||
after: createElement('div', {translate: 'no'}, badge(true), contentsWrap('Hello'), tail),
|
||||
pipeline: chromeFaithfulCompoundPipeline,
|
||||
});
|
||||
expect(result.htmlAfterTranslation).toBe(result.htmlBeforeTranslation);
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.htmlAfterCommit).toContain('<b>!</b>');
|
||||
expect(visibleText(result.htmlAfterCommit)).toBe('!Hellox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shapes that were never at risk', () => {
|
||||
it('does not throw when only elements are siblings', () => {
|
||||
const result = commitUnderTranslation({
|
||||
before: row(badge(false), createElement('i', null, 'a')),
|
||||
after: row(badge(true), createElement('i', null, 'a')),
|
||||
pipeline: TranslatorPipelines.chromeLegacyFont,
|
||||
});
|
||||
expect(result.htmlAfterTranslation).not.toBe(result.htmlBeforeTranslation);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it('does not throw when a sibling is appended after the text', () => {
|
||||
const result = commitUnderTranslation({
|
||||
before: row('Hello', badge(false)),
|
||||
after: row('Hello', badge(true)),
|
||||
pipeline: TranslatorPipelines.chromeLegacyFont,
|
||||
});
|
||||
expect(result.htmlAfterTranslation).not.toBe(result.htmlBeforeTranslation);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,343 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {commitUnderTranslation, isTranslationDomCrash} from '@app/features/i18n/testing/TranslationCommitHarness';
|
||||
import {TranslatorPipelines} from '@app/features/i18n/testing/TranslatorSimulation';
|
||||
import {createElement, type ReactNode} from 'react';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
type GuardModule = typeof import('@app/features/i18n/utils/TranslationDomGuard');
|
||||
type DeveloperOptionsModule = typeof import('@app/features/devtools/state/DeveloperOptions');
|
||||
|
||||
interface LoadedGuard {
|
||||
guard: GuardModule;
|
||||
developerOptions: DeveloperOptionsModule['default'];
|
||||
}
|
||||
|
||||
let loaded: LoadedGuard | null = null;
|
||||
let originalInsertBefore: typeof Node.prototype.insertBefore;
|
||||
let originalRemoveChild: typeof Node.prototype.removeChild;
|
||||
|
||||
async function loadGuard(options: {electron?: boolean; disabled?: boolean} = {}): Promise<LoadedGuard> {
|
||||
vi.resetModules();
|
||||
const {Platform} = await import('@app/features/platform/types/Platform');
|
||||
Platform.isElectron = options.electron === true;
|
||||
const developerOptions = (await import('@app/features/devtools/state/DeveloperOptions')).default;
|
||||
developerOptions.updateOption('disableTranslationDomGuard', options.disabled === true);
|
||||
const guard = await import('@app/features/i18n/utils/TranslationDomGuard');
|
||||
loaded = {guard, developerOptions};
|
||||
return loaded;
|
||||
}
|
||||
|
||||
function translatorDisplacedText(): {container: HTMLElement; text: Text} {
|
||||
const container = document.createElement('div');
|
||||
container.setAttribute('data-flx', 'guard-fixture');
|
||||
const outer = document.createElement('font');
|
||||
const inner = document.createElement('font');
|
||||
const text = document.createTextNode('Hello');
|
||||
inner.appendChild(text);
|
||||
outer.appendChild(inner);
|
||||
container.appendChild(outer);
|
||||
document.body.appendChild(container);
|
||||
return {container, text};
|
||||
}
|
||||
|
||||
function detachedTranslatedText(): {container: HTMLElement; text: Text} {
|
||||
const container = document.createElement('div');
|
||||
const outer = document.createElement('font');
|
||||
const inner = document.createElement('font');
|
||||
const text = document.createTextNode('Hello');
|
||||
inner.appendChild(text);
|
||||
outer.appendChild(inner);
|
||||
container.appendChild(outer);
|
||||
document.body.appendChild(container);
|
||||
document.documentElement.classList.add('translated-ltr');
|
||||
outer.replaceChildren();
|
||||
inner.replaceChildren();
|
||||
return {container, text};
|
||||
}
|
||||
|
||||
function unrelatedSubtreeText(): {container: HTMLElement; other: HTMLElement; text: Text} {
|
||||
const container = document.createElement('div');
|
||||
const other = document.createElement('div');
|
||||
other.id = 'other';
|
||||
const font = document.createElement('font');
|
||||
const text = document.createTextNode('Hello');
|
||||
font.appendChild(text);
|
||||
other.appendChild(font);
|
||||
document.body.append(container, other);
|
||||
return {container, other, text};
|
||||
}
|
||||
|
||||
function foreignParentText(): {container: HTMLElement; text: Text} {
|
||||
const container = document.createElement('div');
|
||||
const elsewhere = document.createElement('div');
|
||||
const text = document.createTextNode('Hello');
|
||||
elsewhere.appendChild(text);
|
||||
document.body.append(container, elsewhere);
|
||||
return {container, text};
|
||||
}
|
||||
|
||||
const badge = (on: boolean): ReactNode => (on ? createElement('b', null, '!') : null);
|
||||
const tail = createElement('i', null, 'x');
|
||||
const unsafeRow = (on: boolean): ReactNode =>
|
||||
createElement('div', null, badge(on), 'Hello', createElement('i', null, 'x'));
|
||||
|
||||
beforeEach(() => {
|
||||
originalInsertBefore = Node.prototype.insertBefore;
|
||||
originalRemoveChild = Node.prototype.removeChild;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loaded?.guard.uninstallTranslationDomGuard();
|
||||
loaded?.developerOptions.updateOption('disableTranslationDomGuard', false);
|
||||
loaded = null;
|
||||
Node.prototype.insertBefore = originalInsertBefore;
|
||||
Node.prototype.removeChild = originalRemoveChild;
|
||||
document.body.replaceChildren();
|
||||
document.documentElement.className = '';
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('installation', () => {
|
||||
it('patches both prototype methods and restores them by identity on uninstall', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
guard.installTranslationDomGuard();
|
||||
expect(Node.prototype.insertBefore).not.toBe(originalInsertBefore);
|
||||
expect(Node.prototype.removeChild).not.toBe(originalRemoveChild);
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(true);
|
||||
|
||||
guard.installTranslationDomGuard();
|
||||
expect(guard.uninstallTranslationDomGuard()).toBe(true);
|
||||
expect(Node.prototype.insertBefore).toBe(originalInsertBefore);
|
||||
expect(Node.prototype.removeChild).toBe(originalRemoveChild);
|
||||
expect(guard.uninstallTranslationDomGuard()).toBe(false);
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(false);
|
||||
});
|
||||
|
||||
it('never installs in Electron', async () => {
|
||||
const {guard} = await loadGuard({electron: true});
|
||||
guard.installTranslationDomGuard();
|
||||
expect(Node.prototype.insertBefore).toBe(originalInsertBefore);
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(false);
|
||||
});
|
||||
|
||||
it('never installs when the Electron native context is unavailable', async () => {
|
||||
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(
|
||||
'Mozilla/5.0 (Macintosh) fluxer/1.0.0 Chrome/140.0.0.0 Electron/38.2.1 Safari/537.36',
|
||||
);
|
||||
const {guard} = await loadGuard({electron: false});
|
||||
guard.installTranslationDomGuard();
|
||||
expect(Node.prototype.insertBefore).toBe(originalInsertBefore);
|
||||
expect(Node.prototype.removeChild).toBe(originalRemoveChild);
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(false);
|
||||
});
|
||||
|
||||
it('never patches while the kill switch is already set', async () => {
|
||||
const {guard} = await loadGuard({disabled: true});
|
||||
guard.installTranslationDomGuard();
|
||||
expect(Node.prototype.insertBefore).toBe(originalInsertBefore);
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(false);
|
||||
});
|
||||
|
||||
it('uninstalls and reinstalls live when the kill switch is toggled', async () => {
|
||||
const {guard, developerOptions} = await loadGuard();
|
||||
guard.installTranslationDomGuard();
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(true);
|
||||
|
||||
developerOptions.updateOption('disableTranslationDomGuard', true);
|
||||
expect(Node.prototype.insertBefore).toBe(originalInsertBefore);
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(false);
|
||||
|
||||
developerOptions.updateOption('disableTranslationDomGuard', false);
|
||||
expect(Node.prototype.insertBefore).not.toBe(originalInsertBefore);
|
||||
expect(guard.getTranslationDomGuardStats().installed).toBe(true);
|
||||
});
|
||||
|
||||
it('exposes stats and a live uninstall on the debug object', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
guard.installTranslationDomGuard();
|
||||
const debug = window.__FLUXER_DEBUG__;
|
||||
const getStats = debug?.getTranslationDomGuardStats as
|
||||
| (() => ReturnType<GuardModule['getTranslationDomGuardStats']>)
|
||||
| undefined;
|
||||
expect(getStats?.().installed).toBe(true);
|
||||
expect(debug?.uninstallTranslationDomGuard?.()).toBe(true);
|
||||
expect(Node.prototype.insertBefore).toBe(originalInsertBefore);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reporting', () => {
|
||||
it('counts and logs every repaired insertBefore', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
guard.installTranslationDomGuard();
|
||||
const {container, text} = translatorDisplacedText();
|
||||
|
||||
expect(() => container.insertBefore(document.createElement('b'), text)).not.toThrow();
|
||||
|
||||
const stats = guard.getTranslationDomGuardStats();
|
||||
expect(stats.insertBefore).toBe(1);
|
||||
expect(stats.removeChild).toBe(0);
|
||||
expect(stats.declined).toBe(0);
|
||||
expect(stats.firstFireAt).not.toBeNull();
|
||||
expect(stats.lastPath).toBe('guard-fixture');
|
||||
const logged = consoleError.mock.calls.flat().join(' ');
|
||||
expect(logged).toContain('TranslationDomGuard');
|
||||
expect(logged).toContain('insertBefore');
|
||||
});
|
||||
|
||||
it('counts and logs every repaired removeChild', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
guard.installTranslationDomGuard();
|
||||
const {container, text} = translatorDisplacedText();
|
||||
|
||||
expect(() => container.removeChild(text)).not.toThrow();
|
||||
|
||||
const stats = guard.getTranslationDomGuardStats();
|
||||
expect(stats.removeChild).toBe(1);
|
||||
expect(stats.insertBefore).toBe(0);
|
||||
expect(text.parentNode).toBeNull();
|
||||
expect(container.innerHTML).toBe('');
|
||||
expect(consoleError.mock.calls.flat().join(' ')).toContain('removeChild');
|
||||
});
|
||||
|
||||
it('keeps counting after it stops logging', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
guard.installTranslationDomGuard();
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
const {container, text} = translatorDisplacedText();
|
||||
container.insertBefore(document.createElement('b'), text);
|
||||
}
|
||||
expect(guard.getTranslationDomGuardStats().insertBefore).toBe(5);
|
||||
expect(consoleError).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('leaves calls on the original path alone', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
guard.installTranslationDomGuard();
|
||||
const parent = document.createElement('div');
|
||||
const child = document.createElement('span');
|
||||
parent.appendChild(child);
|
||||
parent.insertBefore(document.createElement('b'), child);
|
||||
parent.removeChild(child);
|
||||
expect(parent.innerHTML).toBe('<b></b>');
|
||||
const stats = guard.getTranslationDomGuardStats();
|
||||
expect(stats.insertBefore).toBe(0);
|
||||
expect(stats.removeChild).toBe(0);
|
||||
expect(stats.declined).toBe(0);
|
||||
});
|
||||
|
||||
it('declines to repair a displacement no translator caused', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
guard.installTranslationDomGuard();
|
||||
const {container, text} = foreignParentText();
|
||||
|
||||
expect(() => container.insertBefore(document.createElement('b'), text)).toThrow(
|
||||
/Failed to execute 'insertBefore' on 'Node'/,
|
||||
);
|
||||
expect(() => container.removeChild(text)).toThrow(/Failed to execute 'removeChild' on 'Node'/);
|
||||
|
||||
const stats = guard.getTranslationDomGuardStats();
|
||||
expect(stats.insertBefore).toBe(0);
|
||||
expect(stats.removeChild).toBe(0);
|
||||
expect(stats.declined).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the guard behind the structural fix', () => {
|
||||
it('turns the production crash into a reported fire', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const unguarded = commitUnderTranslation({
|
||||
before: unsafeRow(false),
|
||||
after: unsafeRow(true),
|
||||
pipeline: TranslatorPipelines.chromeLegacyFont,
|
||||
});
|
||||
expect(isTranslationDomCrash(unguarded.error)).toBe(true);
|
||||
|
||||
guard.installTranslationDomGuard();
|
||||
const guarded = commitUnderTranslation({
|
||||
before: unsafeRow(false),
|
||||
after: unsafeRow(true),
|
||||
pipeline: TranslatorPipelines.chromeLegacyFont,
|
||||
});
|
||||
expect(guarded.error).toBeNull();
|
||||
expect(guard.getTranslationDomGuardStats().insertBefore).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('repairs the placement at the position the translator displaced', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
guard.installTranslationDomGuard();
|
||||
let firstTag: string | null = null;
|
||||
const result = commitUnderTranslation({
|
||||
before: unsafeRow(false),
|
||||
after: unsafeRow(true),
|
||||
pipeline: TranslatorPipelines.chromeLegacyFont,
|
||||
inspect: (host) => {
|
||||
firstTag = host.querySelector('div')?.firstElementChild?.tagName ?? null;
|
||||
},
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
expect(firstTag).toBe('B');
|
||||
expect(result.htmlAfterCommit.replace(/<[^>]+>/g, '')).toBe('!~Hello~x');
|
||||
});
|
||||
|
||||
it('leaves no empty husk behind after a repaired deletion', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
guard.installTranslationDomGuard();
|
||||
let html = '';
|
||||
let fontCount = -1;
|
||||
let emptyFontCount = -1;
|
||||
const result = commitUnderTranslation({
|
||||
before: createElement('div', null, 'Hello', tail),
|
||||
after: createElement('div', null, null, tail),
|
||||
pipeline: TranslatorPipelines.chromeLegacyFont,
|
||||
inspect: (host) => {
|
||||
html = host.innerHTML;
|
||||
const fonts = Array.from(host.querySelectorAll('font'));
|
||||
fontCount = fonts.length;
|
||||
emptyFontCount = fonts.filter((font) => font.childNodes.length === 0).length;
|
||||
},
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
expect(guard.getTranslationDomGuardStats().removeChild).toBeGreaterThan(0);
|
||||
expect(html.replace(/<[^>]+>/g, '')).toBe('~x');
|
||||
expect(html).not.toContain('Hello');
|
||||
expect(emptyFontCount).toBe(0);
|
||||
expect(fontCount).toBe(2);
|
||||
});
|
||||
|
||||
it('no-ops a removeChild of a node the translator already detached', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
guard.installTranslationDomGuard();
|
||||
const {container, text} = detachedTranslatedText();
|
||||
expect(text.parentNode).toBeNull();
|
||||
|
||||
expect(container.removeChild(text)).toBe(text);
|
||||
|
||||
expect(text.parentNode).toBeNull();
|
||||
expect(guard.getTranslationDomGuardStats().removeChild).toBe(1);
|
||||
expect(consoleError.mock.calls.flat().join(' ')).toContain('removeChild');
|
||||
});
|
||||
|
||||
it('does not unwind husks outside the container the removal was addressed to', async () => {
|
||||
const {guard} = await loadGuard();
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
guard.installTranslationDomGuard();
|
||||
const {container, other, text} = unrelatedSubtreeText();
|
||||
|
||||
expect(container.removeChild(text)).toBe(text);
|
||||
|
||||
expect(text.parentNode).toBeNull();
|
||||
expect(other.parentNode).toBe(document.body);
|
||||
expect(document.getElementById('other')).toBe(other);
|
||||
expect(other.innerHTML).toBe('<font></font>');
|
||||
expect(guard.getTranslationDomGuardStats().removeChild).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user