diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 676757bb..9231f728 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,9 @@ If a feature seems to need a dependency or a build step, open an issue first. ## Development -The frontend is static; edit files under `ui/` and reload. Tests use Node's -built-in runner, with no test dependencies. +The frontend is static; edit files under `ui/` and reload. Unit tests use Node's +built-in runner and need nothing installed; coverage and the browser tests use +the dev tooling in the root `package.json`. An export whose name starts with `_` exists for the tests and is not part of what the module offers: `_internals` for functions reached directly, `_reset*` diff --git a/docs/frontend.md b/docs/frontend.md index 8243d2fe..3d3a23e7 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -34,4 +34,4 @@ Each of these is a single batch request: the server fetches every configured bad ## Cache busting -`?v=` on `/css/` and `/js/` URLs is a content hash rewritten at release by `scripts/bump-cache-busting.js`; do not edit it. `?v=` on `/widgets/` URLs in `widget-types.js` is manual: bump it when you change a widget's own files. +`?v=` is a content hash rewritten at release by `scripts/bump-cache-busting.js`; do not edit one by hand. On `/css/` and `/js/` URLs the script rewrites the literal in the source. Widget iframe URLs are built from the manifest rather than written literally, so the script hashes each widget's entry files into `entryVersions` in its `widget.json`, and `widget-types.js` reads the hash from there when it builds the URL. diff --git a/playwright.config.js b/playwright.config.js index a7330bc7..0a5e7cd5 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,10 +1,10 @@ // @ts-check /* End-to-end tests, run against the container the release publishes. - The unit suites cover logic without a DOM. These cover the things only a - browser shows: that a form saves what it displays, that a drag reorders and - the order survives a reload, that focus stays where it should, and that the - interface mirrors in Persian. + The unit suites cover logic without a DOM. These cover what only a browser + shows: that the admin form saves what it displays, that an edit changes only + what was edited, and that a saved change survives a reload. Drag reordering, + focus handling and the Persian mirroring are not covered here yet. Two browsers on purpose. WebKit is not decoration: the documented support floor is Safari 16.2, and the last two layout and keyboard problems found by diff --git a/ui/test/cache-busting.test.mjs b/ui/test/cache-busting.test.mjs index 1b507d80..89c68f12 100644 --- a/ui/test/cache-busting.test.mjs +++ b/ui/test/cache-busting.test.mjs @@ -99,3 +99,28 @@ test('the workflows call the shared checks rather than listing their own', () => assert.match(workflow, /uses: \.\/\.github\/actions\/checks/, `${name} bypasses the shared checks`); } }); + +/* The documented rule, against the mechanism. + + docs/frontend.md told contributors that the widget `?v=` was manual and to + bump it by hand when a widget's files changed. It has not been manual since + entryVersions existed: the release hashes each entry file into the manifest + and widget-types.js reads it from there. A contributor following the document + would have hand-edited a value the build owns. + + Pinned by mechanism rather than by sentence: what must stay true is that no + widget URL carries a literal stamp and that the manifest is where the value + comes from. */ +test('the widget cache version comes from the manifest, not a hand-written literal', () => { + const src = fs.readFileSync(path.join(root, 'js/widget-types.js'), 'utf8'); + assert.match(src, /entryVersions\?\.\[file\]/, 'widget-types.js no longer reads the manifest hash'); + const literal = /['"`]\/widgets\/[^'"`]*\?v=\d/.exec(src); + assert.equal(literal, null, `a hand-written widget stamp is back: ${literal && literal[0]}`); +}); + +test('the frontend guide describes the mechanism that exists', () => { + const doc = fs.readFileSync(path.join(root, '..', 'docs', 'frontend.md'), 'utf8'); + const section = doc.slice(doc.indexOf('## Cache busting')); + assert.match(section, /entryVersions/, 'the guide does not mention where the widget hash lives'); + assert.doesNotMatch(section, /is manual/, 'the guide tells contributors to bump a stamp the build owns'); +});