At document start (where the app installs the X runtime) there is no <html> element yet, so the lang report never ran and "Follow X" never learned X's language (found live: siteLang stayed null). It now reads lang again at readystatechange/DOMContentLoaded, and a report that fails (no bridge yet) is retried with a bounded backoff. Tests.
75 lines
3 KiB
TypeScript
75 lines
3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { installPageLangReport, LANG_REPORT_RETRIES } from '../src/pageLang.ts';
|
|
|
|
/** A page whose <html lang> never changes, with timers we step by hand. */
|
|
function fakePage(lang: string, parsed = true) {
|
|
const timers: Array<() => void> = [];
|
|
const listeners = new Map<string, () => void>();
|
|
const root = { getAttribute: (name: string) => (name === 'lang' ? lang : null) };
|
|
const document = {
|
|
documentElement: parsed ? root : null,
|
|
addEventListener: (type: string, fn: () => void) => { listeners.set(type, fn); },
|
|
removeEventListener: (type: string) => { listeners.delete(type); },
|
|
};
|
|
const win = {
|
|
document,
|
|
MutationObserver: class { observe() {} disconnect() {} },
|
|
setTimeout: (fn: () => void) => { timers.push(fn); return timers.length; },
|
|
clearTimeout: () => {},
|
|
} as unknown as Window & typeof globalThis;
|
|
const parse = () => { document.documentElement = root; listeners.get('DOMContentLoaded')?.(); };
|
|
return { win, timers, parse, listeners };
|
|
}
|
|
const settle = () => new Promise(resolve => setImmediate(resolve));
|
|
|
|
test('a language report lost at document start (no IPC bridge yet) is sent again', async () => {
|
|
const { win, timers } = fakePage('en-GB');
|
|
const sent: unknown[] = [];
|
|
let bridge = false;
|
|
installPageLangReport(win, async (command, args) => {
|
|
if (!bridge) throw new Error('IPC bridge not ready');
|
|
sent.push([command, args]);
|
|
});
|
|
await settle();
|
|
assert.equal(sent.length, 0);
|
|
assert.equal(timers.length, 1, 'a retry is scheduled');
|
|
bridge = true;
|
|
timers.shift()!();
|
|
await settle();
|
|
assert.deepEqual(sent, [['tab_report', { report: { lang: 'en-GB' } }]]);
|
|
assert.equal(timers.length, 0, 'nothing more once it went through');
|
|
});
|
|
|
|
test('retries are bounded and stop after dispose', async () => {
|
|
const { win, timers } = fakePage('tr');
|
|
let calls = 0;
|
|
const handle = installPageLangReport(win, async () => { calls += 1; throw new Error('down'); });
|
|
for (let i = 0; i < LANG_REPORT_RETRIES + 5; i += 1) { await settle(); timers.shift()?.(); }
|
|
await settle();
|
|
assert.equal(calls, LANG_REPORT_RETRIES + 1);
|
|
assert.equal(timers.length, 0);
|
|
|
|
const second = fakePage('ja');
|
|
let later = 0;
|
|
const h2 = installPageLangReport(second.win, async () => { later += 1; throw new Error('down'); });
|
|
await settle();
|
|
h2.dispose();
|
|
second.timers.shift()?.();
|
|
await settle();
|
|
assert.equal(later, 1, 'no report after dispose');
|
|
handle.dispose();
|
|
});
|
|
|
|
test('installed at document start before <html> exists, it reports once the parser made it', async () => {
|
|
const page = fakePage('en-GB', false);
|
|
const sent: unknown[] = [];
|
|
const handle = installPageLangReport(page.win, async (command, args) => { sent.push([command, args]); });
|
|
await settle();
|
|
assert.equal(sent.length, 0);
|
|
page.parse();
|
|
await settle();
|
|
assert.deepEqual(sent, [['tab_report', { report: { lang: 'en-GB' } }]]);
|
|
handle.dispose();
|
|
assert.equal(page.listeners.size, 0);
|
|
});
|