| [70677e96] | 1 | // The Guardian PWA is built in the browser from a state blob, so the usual
|
|---|
| 2 | // tests cannot reach it. These two failure modes bit during the rebuild and are
|
|---|
| 3 | // cheap to guard: a renderer writing into a section that no longer exists, and
|
|---|
| 4 | // a grouping key quietly dropped from a route so every panel comes up empty.
|
|---|
| 5 | import { test } from 'node:test';
|
|---|
| 6 | import assert from 'node:assert/strict';
|
|---|
| 7 | import fs from 'fs';
|
|---|
| 8 |
|
|---|
| 9 | const read = (p) => fs.readFileSync(new URL(p, import.meta.url), 'utf8');
|
|---|
| 10 | const client = read('../src/assets/js/guardian.js');
|
|---|
| 11 | const page = read('../src/views/pages/guardian.ejs');
|
|---|
| 12 | const route = read('../src/routes/guardian.js');
|
|---|
| 13 |
|
|---|
| 14 | test('the client only touches element ids the page actually has', () => {
|
|---|
| 15 | // g-fatal is the exception: the crash banner is created by the client itself
|
|---|
| 16 | // (getElementById || createElement), precisely because the page may be too
|
|---|
| 17 | // broken to have it.
|
|---|
| 18 | const ids = new Set([...page.matchAll(/id="([^"]+)"/g)].map((m) => m[1])).add('g-fatal');
|
|---|
| 19 | const used = [...client.matchAll(/getElementById\('([^']+)'\)/g)].map((m) => m[1]);
|
|---|
| 20 | const missing = [...new Set(used)].filter((id) => !ids.has(id));
|
|---|
| 21 | assert.deepEqual(missing, [], `guardian.js writes into ${missing.join(', ')}, which the page does not have`);
|
|---|
| 22 | });
|
|---|
| 23 |
|
|---|
| 24 | test('the sections that moved into the panels are gone from the page', () => {
|
|---|
| 25 | for (const id of ['feed-section', 'follow-section', 'feed-list', 'follow-list']) {
|
|---|
| 26 | assert.ok(!page.includes(`id="${id}"`), `${id} moved into the per-ward panel and must not be a section of its own`);
|
|---|
| 27 | }
|
|---|
| 28 | });
|
|---|
| 29 |
|
|---|
| 30 | test('every panel section has something to group by', () => {
|
|---|
| 31 | // A child's panel is filled by matching these against the ward's actor URI.
|
|---|
| 32 | // Lose one and that section silently shows "nothing yet" for every child.
|
|---|
| 33 | assert.match(route, /authorUri: p\.author_uri/, "the wards' posts");
|
|---|
| 34 | assert.match(route, /wardUri: w\.uri/, 'follow requests on a local ward');
|
|---|
| 35 | assert.match(route, /wardUri: rev\.ward_uri/, 'follow requests forwarded from a remote ward');
|
|---|
| 36 | for (const key of ['authorUri', 'wardUri', 'actor_uri']) {
|
|---|
| 37 | assert.ok(client.includes(key), `the panel groups on ${key}`);
|
|---|
| 38 | }
|
|---|
| 39 | });
|
|---|
| 40 |
|
|---|
| [742ba7e] | 41 | test('releasing a ward is two steps, not a browser confirm', () => {
|
|---|
| 42 | // Robins besluit: letting a child go is a decision. window.confirm hides a
|
|---|
| 43 | // long explanation behind an OK button people press to make it go away.
|
|---|
| 44 | // A call, not the word: the comment above the release button names it too.
|
|---|
| 45 | assert.ok(!/window\.confirm\s*\(/.test(client), 'the release must ask in the panel, with its own yes and no');
|
|---|
| 46 | assert.match(client, /release-check/, 'and it first asks the server what releasing this ward actually does');
|
|---|
| 47 | for (const k of ['release_last', 'release_step_down', 'release_unknown']) {
|
|---|
| 48 | assert.ok(client.includes(k), `the warning must cover the ${k} case`);
|
|---|
| 49 | }
|
|---|
| 50 | assert.match(route, /not_my_ward/, 'and the check refuses a ward that is not yours');
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | test('every CSS variable the PWA uses is one it defines', () => {
|
|---|
| 54 | // The dashboard is standalone: it never inherits the site theme, so an
|
|---|
| 55 | // undefined var silently renders as nothing. --danger did exactly that: the
|
|---|
| 56 | // warning box lost its border and the confirm button its background.
|
|---|
| 57 | const css = read('../src/assets/css/guardian.css');
|
|---|
| 58 | const root = (css.match(/:root\s*\{([\s\S]*?)\}/) || [])[1] || '';
|
|---|
| 59 | const defined = new Set([...root.matchAll(/(--[a-z0-9-]+)\s*:/g)].map((m) => m[1]));
|
|---|
| 60 | const used = [...new Set([...css.matchAll(/var\((--[a-z0-9-]+)(\s*,)?/g)]
|
|---|
| 61 | .filter((m) => !m[2]) // one with a fallback is fine
|
|---|
| 62 | .map((m) => m[1]))];
|
|---|
| 63 | const missing = used.filter((v) => !defined.has(v));
|
|---|
| 64 | assert.deepEqual(missing, [], `guardian.css uses ${missing.join(', ')} without defining it`);
|
|---|
| 65 | });
|
|---|
| 66 |
|
|---|
| [70677e96] | 67 | test('the labels the panel renders are all served to it', () => {
|
|---|
| 68 | // uiStrings() picks the keys by hand, so a label used in the client but not
|
|---|
| 69 | // listed there renders as an empty string with no error anywhere. Two shapes
|
|---|
| 70 | // count as served: an entry in the keys array, and a direct s.foo = ...
|
|---|
| 71 | // assignment underneath it (wave/waved arrived that way).
|
|---|
| 72 | const served = new Set([
|
|---|
| 73 | ...[...route.matchAll(/'([a-z_]+)'/g)].map((m) => m[1]),
|
|---|
| 74 | ...[...route.matchAll(/\bs\.([a-z_]+)\s*=/g)].map((m) => m[1]),
|
|---|
| 75 | ]);
|
|---|
| 76 | const used = [...new Set([...client.matchAll(/T\.([a-z_]+)/g)].map((m) => m[1]))];
|
|---|
| 77 | const missing = used.filter((k) => !served.has(k));
|
|---|
| 78 | assert.deepEqual(missing, [], `uiStrings() does not serve: ${missing.join(', ')}`);
|
|---|
| 79 | });
|
|---|