test: stub navigator so the i18n test runs on Node 20 too

i18n.js sniffs navigator.language at module load. Node has had navigator as a global
only since 21, so the test passed locally on Node 24 and failed in CI on Node 20 with
"navigator is not defined" — a green local run that proved nothing about the version
CI actually uses. Stubbed explicitly, and verified on both 20 and 24 this time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56
This commit is contained in:
ScreenTinker 2026-08-18 23:00:56 -05:00
parent 9823aaf595
commit 68c7903cc8

View file

@ -85,16 +85,25 @@ test('the interpolation resolves, and falls back to the product name', async ()
// i18n.js reads window.__ST_BRAND_NAME at CALL time, so branding.js can refresh it after first // i18n.js reads window.__ST_BRAND_NAME at CALL time, so branding.js can refresh it after first
// paint and a workspace switch shows the new brand rather than the one cached at module load. // paint and a workspace switch shows the new brand rather than the one cached at module load.
const mod = path.join(I18N_DIR, '..', 'i18n.js'); const mod = path.join(I18N_DIR, '..', 'i18n.js');
// i18n.js reads the saved language from localStorage as it loads, so both browser globals have to /*
// exist before the import — not because this test cares about storage, but because the module * i18n.js reads the saved language from localStorage AND sniffs navigator.language as it loads,
// would throw on the way in. * so both browser globals have to exist before the import not because this test cares about
* either, but because the module throws on the way in without them.
*
* navigator is the one that bites: Node has had it as a global only since 21, so this passed
* locally on Node 24 and failed in CI on Node 20 with "navigator is not defined". Stubbing it
* explicitly makes the test independent of which Node happens to be running it.
*/
const store = new Map(); const store = new Map();
globalThis.localStorage = { globalThis.localStorage = {
getItem: (k) => (store.has(k) ? store.get(k) : null), getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)), setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k), removeItem: (k) => store.delete(k),
}; };
globalThis.window = { __ST_BRAND_NAME: undefined, localStorage: globalThis.localStorage }; const priorNavigator = globalThis.navigator;
if (!priorNavigator) globalThis.navigator = { language: 'en-US', languages: ['en-US'] };
globalThis.window = { __ST_BRAND_NAME: undefined, localStorage: globalThis.localStorage,
navigator: globalThis.navigator };
const { t } = await import(`file://${mod}`); const { t } = await import(`file://${mod}`);
const unbranded = t('settings.setup_step_1'); const unbranded = t('settings.setup_step_1');
@ -112,4 +121,5 @@ test('the interpolation resolves, and falls back to the product name', async ()
assert.ok(t('settings.setup_step_1').includes('ScreenTinker')); assert.ok(t('settings.setup_step_1').includes('ScreenTinker'));
delete globalThis.window; delete globalThis.window;
delete globalThis.localStorage; delete globalThis.localStorage;
if (!priorNavigator) delete globalThis.navigator;
}); });