diff --git a/README.md b/README.md index 151f474..a9a2791 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,13 @@ red PR job against. - `node --test scripts/page-scripts.test.mjs` — static gate over every committed HTML page: each inline `\n\n\n`; + writeFileSync(join(dir, "index.html"), page("[data-dead]")); + writeFileSync(join(dir, "w", "index.html"), page("[data-live]")); + const helper = pathToFileURL(join(HERE, "inline-scripts.mjs")).href; + const harness = (pageRel, hook, boots) => `import { test } from "node:test";\nimport assert from "node:assert/strict";\nimport { readFileSync } from "node:fs";\nimport { inlineScript } from ${JSON.stringify(helper)};\nconst { src } = inlineScript(readFileSync(${JSON.stringify(join(dir, pageRel))}, "utf8"), ${JSON.stringify(pageRel)}, ${JSON.stringify(hook)});\nfunction boot(root) { new Function("document", src)({ querySelector: () => root }); return root; }\n${boots}\n`; + const el = (on) => `{ attrs: ${on ? "{ 'data-on': '1' }" : "{}"}, getAttribute(k) { return this.attrs[k]; }, className: "" }`; + writeFileSync(join(dir, "scripts", "dead.test.mjs"), harness("index.html", "[data-dead]", `test("on", () => assert.equal(boot(${el(true)}).className, "on"));`)); + writeFileSync(join(dir, "scripts", "live.test.mjs"), harness("w/index.html", "[data-live]", `test("on", () => assert.equal(boot(${el(true)}).className, "on"));\ntest("off", () => assert.equal(boot(${el(false)}).className, "off"));\ntest("absent", () => assert.equal(boot(null), null));`)); + const registry = { "index.html": [{ hook: "[data-dead]", harness: "scripts/dead.test.mjs" }], "w/index.html": [{ hook: "[data-live]", harness: "scripts/live.test.mjs" }] }; + const sources = inlineSources(dir, registry); + assert.deepEqual([...sources.keys()], ["index.html#[data-dead]", "w/index.html#[data-live]"]); + assert.equal(sources.get("index.html#[data-dead]").line, 4); + const { reports, status, stdout, stderr } = collect(dir, suiteFiles(dir)); + assert.equal(status, 0, `${stdout}\n${stderr}`); + const { runs, problems } = inlineRunsFromReports(reports, sources); + assert.deepEqual(problems, []); + assert.deepEqual([...runs.keys()].sort(), ["index.html#[data-dead]", "w/index.html#[data-live]"]); + assert.deepEqual(problemsOf(sources, runs), [ + "index.html#[data-dead]:7 `return;` never executed by the suite (add a test that asserts what this branch decides)", + "index.html#[data-dead]:8 `: 'off'` never executed by the suite (add a test that asserts what this branch decides)", + ]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + // --- live gate -------------------------------------------------------------- -test("every branch of every scripts/*.mjs helper is executed by the suite", { skip: process.env[CHILD_ENV] ? "running inside the coverage-floor child" : false }, () => { +test("every branch of every scripts/*.mjs helper and every inline page script is executed by the suite", { skip: process.env[CHILD_ENV] ? "running inside the coverage-floor child" : false }, () => { const sources = helperSources(ROOT); assert.ok(sources.size > 0, "no helpers under scripts/"); + const inline = inlineSources(ROOT); + assert.ok(inline.size > 0, "no inline scripts registered in HARNESSED_SCRIPTS"); const files = suiteFiles(ROOT); assert.ok(!files.includes(`scripts/${SELF}`)); const { reports, status, stdout, stderr } = collect(ROOT, files); assert.equal(status, 0, `child suite failed; fix it first\n${stdout}\n${stderr}`); - assert.deepEqual(problemsOf(sources, runsFromReports(reports, ROOT, sources)), []); + const inlineRuns = inlineRunsFromReports(reports, inline); + assert.deepEqual([ + ...problemsOf(sources, runsFromReports(reports, ROOT, sources)), + ...inlineRuns.problems, + ...problemsOf(inline, inlineRuns.runs), + ], []); }); diff --git a/scripts/inline-scripts.mjs b/scripts/inline-scripts.mjs new file mode 100644 index 0000000..b9e0f9f --- /dev/null +++ b/scripts/inline-scripts.mjs @@ -0,0 +1,36 @@ +// Registry of the inline behaviour scripts the pages ship and the harness test +// that extracts and runs each one. page-scripts.test.mjs requires an entry per +// block, the harnesses extract their block through `inlineScript`, and +// coverage-floor.test.mjs reads the V8 coverage of each block under the URL +// `inlineScript` tags it with — so a branch added to a page script that no +// harness test asserts fails the floor the same way a helper branch does. +export const HARNESSED_SCRIPTS = { + "index.html": [ + { hook: "[data-hero-carousel]", harness: "scripts/carousels.test.mjs" }, + { hook: "[data-acmm-levels]", harness: "scripts/acmm-levels.test.mjs" }, + { hook: "[data-carousel]", harness: "scripts/carousels.test.mjs" }, + ], + "stories/index.html": [ + { hook: 'getElementById("story-dialog")', harness: "scripts/story-dialog.test.mjs" }, + ], +}; + +export const INLINE_URL_PREFIX = "inline:"; + +// The script URL V8 reports for a block run via `new Function`: a `//# sourceURL` +// trailer names the function body, which otherwise has the empty URL. +export function inlineUrl(page, hook) { + return `${INLINE_URL_PREFIX}${page}#${hook}`; +} + +// The single plain `\n

x

\n\n\n"; + const { src, line } = inlineScript(html, "w/index.html", "[data-w]"); + assert.equal(line, 5); + assert.equal(src, "\n(function () { var r = document.querySelector('[data-w]'); if (!r) return; })();\n\n//# sourceURL=inline:w/index.html#[data-w]\n"); + assert.throws(() => inlineScript(html, "w/index.html", "[data-missing]"), /w\/index\.html: expected exactly one diff --git a/scripts/story-dialog.test.mjs b/scripts/story-dialog.test.mjs index 4e312dc..1b6823f 100644 --- a/scripts/story-dialog.test.mjs +++ b/scripts/story-dialog.test.mjs @@ -10,16 +10,14 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { inlineScript } from "./inline-scripts.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const PAGE = join(HERE, "..", "stories", "index.html"); const STORY_MAX = 1500; function extractDialogScript(html) { - const blocks = [...html.matchAll(/