From f439aa8a17b17b1347c993c6e856a1b3c5972e79 Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Thu, 7 May 2026 11:28:34 -0700 Subject: [PATCH 1/3] Expand test coverage using node:test Test src/translations.js directly (not the babel-built dist) and cover all three exported functions including window-dependent branches and the uninitialized-translations fallback path. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- test/test-uninitialized.mjs | 19 ++++++++ test/test.mjs | 87 ++++++++++++++++++++++++++++++++++--- 3 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 test/test-uninitialized.mjs diff --git a/package.json b/package.json index 9893263..3f54194 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dist", "localize.d.ts" ], "scripts": { - "test": "node ./test/test.mjs", + "test": "node --test test/", "build": "rimraf dist && NODE_ENV=production babel src --out-dir dist --copy-files --ignore __tests__,spec.js,test.js,__snapshots__" }, "devDependencies": { diff --git a/test/test-uninitialized.mjs b/test/test-uninitialized.mjs new file mode 100644 index 0000000..84a359d --- /dev/null +++ b/test/test-uninitialized.mjs @@ -0,0 +1,19 @@ +import { test } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { _js } from '../src/translations.js'; + +test('_js: falls back to returning the input string when no translations are set', (t) => { + const errors = []; + t.mock.method(console, 'error', (msg) => errors.push(msg)); + assert.equal(_js('Hello'), 'Hello'); + assert.equal(errors.length, 1); + assert.match(errors[0], /UI Translations not found/); +}); + +test('_js: only logs the missing-translations warning once', (t) => { + const errors = []; + t.mock.method(console, 'error', (msg) => errors.push(msg)); + _js('first'); + _js('second'); + assert.equal(errors.length, 0); +}); diff --git a/test/test.mjs b/test/test.mjs index 8869775..62ad3e5 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -1,7 +1,84 @@ -import { _js, setTranslations } from "../dist/translations.js"; -import { strict as assert } from 'assert'; +import { test, beforeEach, afterEach } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { _js, ___p, setTranslations } from '../src/translations.js'; -const translatedHi = "Translated Hi (%1)"; +beforeEach(() => { + setTranslations({}); + delete globalThis.window; +}); -setTranslations({Hi: translatedHi}); -assert.equal("Translated Hi (param)", _js("Hi", "param")); +afterEach(() => { + delete globalThis.window; +}); + +test('_js: returns the input string when no translation matches', () => { + assert.equal(_js('Hello'), 'Hello'); +}); + +test('_js: returns the translated string when a match is found', () => { + setTranslations({ Hello: 'Hola' }); + assert.equal(_js('Hello'), 'Hola'); +}); + +test('_js: collapses runs of whitespace before lookup', () => { + setTranslations({ 'Foo bar baz': 'translated' }); + assert.equal(_js('Foo bar\n\tbaz'), 'translated'); +}); + +test('_js: returns the compacted string when no match found', () => { + assert.equal(_js('Foo bar'), 'Foo bar'); +}); + +test('_js: substitutes a single %1 placeholder', () => { + setTranslations({ Hi: 'Translated Hi (%1)' }); + assert.equal(_js('Hi', 'param'), 'Translated Hi (param)'); +}); + +test('_js: substitutes multiple positional placeholders', () => { + assert.equal(_js('a %1 b %2', 'x', 'y'), 'a x b y'); +}); + +test('_js: replaces every occurrence of a placeholder', () => { + assert.equal(_js('%1 and %1', 'foo'), 'foo and foo'); +}); + +test('_js: coerces non-string input via String()', () => { + assert.equal(_js(42), '42'); +}); + +test('_js: returns "Translated" when window.App.showTranslatedPlaceholder is set', () => { + globalThis.window = { App: { showTranslatedPlaceholder: true } }; + setTranslations({ x: 'translated x' }); + assert.equal(_js('x'), 'Translated'); +}); + +test('_js: ignores window.App when the placeholder flag is falsy', () => { + globalThis.window = { App: { showTranslatedPlaceholder: false } }; + setTranslations({ x: 'translated x' }); + assert.equal(_js('x'), 'translated x'); +}); + +test('___p: uses the singular form when number === 1', () => { + assert.equal(___p(1, '%1 step', '%1 steps', 1), '1 step'); +}); + +test('___p: uses the plural form when number === 0', () => { + assert.equal(___p(0, '%1 step', '%1 steps', 0), '0 steps'); +}); + +test('___p: uses the plural form when number > 1', () => { + assert.equal(___p(5, '%1 step', '%1 steps', 5), '5 steps'); +}); + +test('___p: forwards args to _js for translation lookup', () => { + setTranslations({ '%1 step': 'TS:%1', '%1 steps': 'TP:%1' }); + assert.equal(___p(1, '%1 step', '%1 steps', 7), 'TS:7'); + assert.equal(___p(2, '%1 step', '%1 steps', 7), 'TP:7'); +}); + +test('setTranslations: replaces the translation table on each call', () => { + setTranslations({ a: 'first' }); + assert.equal(_js('a'), 'first'); + setTranslations({ a: 'second' }); + assert.equal(_js('a'), 'second'); +}); From 023caa4c99121a3ebe5f730030b9b7892abe2313 Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Thu, 7 May 2026 12:01:54 -0700 Subject: [PATCH 2/3] Add type: module to quite node during tests It says this is a module type but complains since we don't declare it. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3f54194..4a4d278 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "homepage": "https://github.com/iFixit/localize", "main": "dist/translations.js", "types": "./localize.d.ts", + "type": "module", "module": "dist/translations.js", "files": [ "dist", "localize.d.ts" From 9275b3cfa17011bf570408cc8e322d2221518bb6 Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Thu, 7 May 2026 12:02:02 -0700 Subject: [PATCH 3/3] Fix uninitialized test to clear state Without clearing the state to undefined, we weren't actually seeing the errors get added. --- test/test-uninitialized.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/test-uninitialized.mjs b/test/test-uninitialized.mjs index 84a359d..65a2aeb 100644 --- a/test/test-uninitialized.mjs +++ b/test/test-uninitialized.mjs @@ -1,6 +1,11 @@ -import { test } from 'node:test'; +import { test, beforeEach } from 'node:test'; import { strict as assert } from 'node:assert'; -import { _js } from '../src/translations.js'; +import { _js, setTranslations } from '../src/translations.js'; + +beforeEach(() => { + // Reset the translations state before each test + setTranslations(undefined); +}); test('_js: falls back to returning the input string when no translations are set', (t) => { const errors = []; @@ -15,5 +20,5 @@ test('_js: only logs the missing-translations warning once', (t) => { t.mock.method(console, 'error', (msg) => errors.push(msg)); _js('first'); _js('second'); - assert.equal(errors.length, 0); + assert.equal(errors.length, 1); });