From 68c7903cc830b0ed0d9222147f3f532634101287 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Tue, 18 Aug 2026 23:00:56 -0500 Subject: [PATCH] test: stub navigator so the i18n test runs on Node 20 too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56 --- server/test/brand-name-i18n.test.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/server/test/brand-name-i18n.test.js b/server/test/brand-name-i18n.test.js index b8dc6f1..96c38e3 100644 --- a/server/test/brand-name-i18n.test.js +++ b/server/test/brand-name-i18n.test.js @@ -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 // 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'); - // 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 - // would throw on the way in. + /* + * i18n.js reads the saved language from localStorage AND sniffs navigator.language as it loads, + * 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(); globalThis.localStorage = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), 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 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')); delete globalThis.window; delete globalThis.localStorage; + if (!priorNavigator) delete globalThis.navigator; });