From 13969c7070034c1b321998e466f9e5a128ace44c Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 20 Jun 2026 16:33:36 -0400 Subject: refactor(theme-studio): dedup the inline-integrity test scaffolding Two test-DRY cleanups. The seven near-identical test_page_carries_*_verbatim methods in test_generate.py collapse into one subTest loop over the inlined-body names. The strip-exports helper -- reimplemented three times across the colormath, app-core, and app-util inline-integrity tests, each annotated 'same strip generate.py applies' -- moves to one shared inline-strip.mjs (stripInlinedBody), so the three copies can no longer drift from generate.py's strip_exports. --- scripts/theme-studio/inline-strip.mjs | 15 ++++++++++++++ scripts/theme-studio/test-app-core.mjs | 13 ++++++------ scripts/theme-studio/test-app-util.mjs | 6 ++---- scripts/theme-studio/test-colormath.mjs | 7 +++---- scripts/theme-studio/test_generate.py | 36 ++++++++++----------------------- 5 files changed, 37 insertions(+), 40 deletions(-) create mode 100644 scripts/theme-studio/inline-strip.mjs (limited to 'scripts') diff --git a/scripts/theme-studio/inline-strip.mjs b/scripts/theme-studio/inline-strip.mjs new file mode 100644 index 000000000..112d55ce6 --- /dev/null +++ b/scripts/theme-studio/inline-strip.mjs @@ -0,0 +1,15 @@ +// Shared by the inline-integrity tests (test-colormath.mjs, test-app-core.mjs, +// test-app-util.mjs). Mirrors strip_exports in generate.py: drop top-level +// export/import lines (a pure module may import a peer for its own unit tests, +// while the inlined page copy relies on that peer already being present), then +// rstrip. The page is asserted to carry the stripped body verbatim, so this MUST +// stay aligned with generate.py's strip_exports -- one definition keeps the three +// test copies from drifting apart. +// +// (This file matches the `*.mjs` test glob in run-tests.sh; it carries no tests, +// so it contributes zero to the count.) +export const stripInlinedBody = (s) => + s.split('\n') + .filter((l) => !(l.startsWith('export') || l.startsWith('import'))) + .join('\n') + .replace(/\s+$/, ''); diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs index 09ddf5aec..217ea0e6b 100644 --- a/scripts/theme-studio/test-app-core.mjs +++ b/scripts/theme-studio/test-app-core.mjs @@ -819,35 +819,34 @@ test('slugify: Error — an all-disallowed name falls back to "theme"', () => { // Guards the one-source-of-truth contract, same as the colormath integrity test: // the page must carry app-core.js's body (sans exports) verbatim. Requires // `python3 generate.py` to have run first. -const stripExports = (s) => - s.split('\n').filter((l) => !(l.startsWith('export') || l.startsWith('import'))).join('\n').replace(/\s+$/, ''); +import { stripInlinedBody } from './inline-strip.mjs'; test('inline-integrity: theme-studio.html contains the app-core.js body verbatim', () => { - const body = stripExports(readFileSync(here + 'app-core.js', 'utf8')); + const body = stripInlinedBody(readFileSync(here + 'app-core.js', 'utf8')); const html = readFileSync(here + 'theme-studio.html', 'utf8'); assert.ok(html.includes(body), 'generated page is missing the app-core.js body verbatim'); }); test('inline-integrity: theme-studio.html contains palette-generator-core.js verbatim', () => { - const body = stripExports(readFileSync(here + 'palette-generator-core.js', 'utf8')); + const body = stripInlinedBody(readFileSync(here + 'palette-generator-core.js', 'utf8')); const html = readFileSync(here + 'theme-studio.html', 'utf8'); assert.ok(html.includes(body), 'generated page is missing palette-generator-core.js verbatim'); }); test('inline-integrity: theme-studio.html contains palette-generator-ui.js verbatim', () => { - const body = stripExports(readFileSync(here + 'palette-generator-ui.js', 'utf8')); + const body = stripInlinedBody(readFileSync(here + 'palette-generator-ui.js', 'utf8')); const html = readFileSync(here + 'theme-studio.html', 'utf8'); assert.ok(html.includes(body), 'generated page is missing palette-generator-ui.js verbatim'); }); test('inline-integrity: theme-studio.html contains palette-actions.js verbatim', () => { - const body = stripExports(readFileSync(here + 'palette-actions.js', 'utf8')); + const body = stripInlinedBody(readFileSync(here + 'palette-actions.js', 'utf8')); const html = readFileSync(here + 'theme-studio.html', 'utf8'); assert.ok(html.includes(body), 'generated page is missing palette-actions.js verbatim'); }); test('inline-integrity: theme-studio.html contains browser-gates.js verbatim', () => { - const body = stripExports(readFileSync(here + 'browser-gates.js', 'utf8')); + const body = stripInlinedBody(readFileSync(here + 'browser-gates.js', 'utf8')); const html = readFileSync(here + 'theme-studio.html', 'utf8'); assert.ok(html.includes(body), 'generated page is missing browser-gates.js verbatim'); }); diff --git a/scripts/theme-studio/test-app-util.mjs b/scripts/theme-studio/test-app-util.mjs index 37cf0889b..057f55f8d 100644 --- a/scripts/theme-studio/test-app-util.mjs +++ b/scripts/theme-studio/test-app-util.mjs @@ -84,12 +84,10 @@ test('textOn: Boundary — straddles the ~0.179 luminance crossover', () => { // Inline-integrity: the page must carry app-util.js's body (sans import/export) // verbatim — the same strip generate.py applies. Requires `python3 generate.py`. -const stripModule = (s) => - s.split('\n').filter((l) => !(l.startsWith('export') || l.startsWith('import'))) - .join('\n').replace(/\s+$/, ''); +import { stripInlinedBody } from './inline-strip.mjs'; test('inline-integrity: theme-studio.html contains the app-util.js body verbatim', () => { - const body = stripModule(readFileSync(here + 'app-util.js', 'utf8')); + const body = stripInlinedBody(readFileSync(here + 'app-util.js', 'utf8')); const html = readFileSync(here + 'theme-studio.html', 'utf8'); assert.ok(html.includes(body), 'generated page is missing the app-util.js body verbatim'); }); diff --git a/scripts/theme-studio/test-colormath.mjs b/scripts/theme-studio/test-colormath.mjs index ee40e3437..a1ec9264e 100644 --- a/scripts/theme-studio/test-colormath.mjs +++ b/scripts/theme-studio/test-colormath.mjs @@ -18,9 +18,8 @@ import { const close = (a, b, eps = 0.005) => Math.abs(a - b) <= eps; const here = fileURLToPath(new URL('.', import.meta.url)); -// Same export-strip generate.py applies before inlining (drop `export` lines, rstrip). -const stripExports = (s) => - s.split('\n').filter((l) => !l.startsWith('export')).join('\n').replace(/\s+$/, ''); +// Same strip generate.py applies before inlining (drop export/import lines, rstrip). +import { stripInlinedBody } from './inline-strip.mjs'; test('srgb2oklab achromatic anchors', () => { const w = srgb2oklab('#ffffff'); @@ -266,7 +265,7 @@ test('reliefColors: malformed hex returns null pair (Error)', () => { // body (sans exports) verbatim, so the inlined copy and the tested module cannot // drift. Requires `python3 generate.py` to have run first. test('inline-integrity: theme-studio.html contains the colormath.js body verbatim', () => { - const body = stripExports(readFileSync(here + 'colormath.js', 'utf8')); + const body = stripInlinedBody(readFileSync(here + 'colormath.js', 'utf8')); const html = readFileSync(here + 'theme-studio.html', 'utf8'); assert.ok(html.includes(body), 'generated page is missing the colormath.js body verbatim'); }); diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index 40956917e..974fca68a 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -102,31 +102,17 @@ class AssembledPage(unittest.TestCase): self.assertIn("keyword", generate.SYNTAX_DOCS["kw"].lower()) self.assertIn(json.dumps(generate.SYNTAX_DOCS), generate.HTML) - def test_page_carries_the_colormath_body_verbatim(self): - # Python-side inline-integrity: the same guarantee the JS test asserts, but - # checked at the point the page is built rather than after a round-trip. - self.assertIn(generate.COLORMATH_BODY, generate.HTML) - - def test_page_carries_the_app_core_body_verbatim(self): - # app-core.js inlines verbatim (no data placeholders), so the inlined copy - # and the unit-tested module cannot drift. - self.assertIn(generate.APP_CORE_BODY, generate.HTML) - - def test_page_carries_the_app_util_body_verbatim(self): - # app-util.js inlines verbatim after its import line is stripped. - self.assertIn(generate.APP_UTIL_BODY, generate.HTML) - - def test_page_carries_palette_generator_core_verbatim(self): - self.assertIn(generate.PALETTE_GENERATOR_CORE_BODY, generate.HTML) - - def test_page_carries_palette_generator_ui_verbatim(self): - self.assertIn(generate.PALETTE_GENERATOR_UI_BODY, generate.HTML) - - def test_page_carries_palette_actions_verbatim(self): - self.assertIn(generate.PALETTE_ACTIONS_BODY, generate.HTML) - - def test_page_carries_browser_gates_verbatim(self): - self.assertIn(generate.BROWSER_GATES_BODY, generate.HTML) + def test_page_carries_each_inlined_body_verbatim(self): + # Python-side inline-integrity: every verbatim-inlined module (no data + # placeholders, exports/imports stripped) must appear in the page byte for + # byte, so the inlined copy and the unit-tested module cannot drift. Checked + # at build time rather than after a round-trip. app-util.js's import line is + # already stripped in APP_UTIL_BODY. + for name in ("COLORMATH_BODY", "APP_CORE_BODY", "APP_UTIL_BODY", + "PALETTE_GENERATOR_CORE_BODY", "PALETTE_GENERATOR_UI_BODY", + "PALETTE_ACTIONS_BODY", "BROWSER_GATES_BODY"): + with self.subTest(body=name): + self.assertIn(getattr(generate, name), generate.HTML) def test_app_util_inlined_body_has_no_import_line(self): # The `import rl` line must be gone, or the page