Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*`
Expand Down
2 changes: 1 addition & 1 deletion docs/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 4 additions & 4 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 25 additions & 0 deletions ui/test/cache-busting.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Loading