source: Klonkt/test/guardian-panel.test.js@ 3ccca13

main
Last change on this file since 3ccca13 was 742ba7e, checked in by Robin Genis <roboburr@…>, 6 weeks ago

Een ward loslaten vraagt eerst, en zegt wat het doet

Loslaten zat achter een window.confirm met een lap tekst erin. Dat is precies
het soort dialoog dat mensen wegklikken om ervan af te zijn, terwijl dit de
zwaarste knop in de app is. Nu is het een stap in het paneel: eerst de gevolgen,
dan ja of nee, met nee links zodat de uitweg de makkelijke is om te raken.

De waarschuwing wordt opgebouwd uit wat er echt aan de hand is, niet uit een
vaste zin, want loslaten betekent twee verschillende dingen. Blijven er andere
guardians over, dan stap jij op en blijft het kind een ward (FEP-633c 3.3). Ben
je de laatste, dan is het emancipatie, en 3.4 zegt uitdrukkelijk dat geen enkele
guardian daar alleen over gaat: drie instemmende volwassenen, of een meerderheid
met twee getuigen. Een nieuwe route zoekt dat op het moment van drukken op, want
voor een ward die wij niet hosten moet daarvoor die server bevraagd worden en
dat hoort niet bij elke ververs-ronde.

En het derde ding dat er hoorde te staan: het Undo federeert nog niet (fase 4 in
relations.js), dus vandaag haalt de knop de band alleen hier weg terwijl de
server van het kind je gewoon als guardian blijft noemen. Zonder die regel drukt
een guardian erop in de overtuiging dat het kind losgelaten is.

Changed files:
src/routes/guardian.js

  • GET /guardian/wards/release-check: telt de guardians van dit kind (lokaal uit de relaties, remote uit shaer:guardians van de actor) en weigert een ward die niet van jou is
  • de nieuwe labels toegevoegd aan uiStrings

src/assets/js/guardian.js

  • releaseStep: de waarschuwing plus ja/nee, in plaats van window.confirm
  • de site gaat mee in de check, anders kijkt de server naar de verkeerde

src/assets/css/guardian.css

  • --danger toegevoegd; die bestond niet, waardoor het kader en de knop onzichtbaar bleven
  • opmaak voor het waarschuwingsblok

src/services/i18n.js

  • 24 strings voor de waarschuwing in nl, en, de

test/guardian-panel.test.js

  • loslaten gaat in twee stappen en niet via window.confirm
  • elke CSS-variabele die de PWA gebruikt is er ook een die hij definieert

remarks: alle vier de gevallen in de browser nagelopen: laatste guardian,
meerdere guardians, onbereikbare server, en een ward die niet van je is. Nee
zet de knop terug zonder iets te doen, ja verwijdert alleen die ene relatie.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 4.4 KB
RevLine 
[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.
5import { test } from 'node:test';
6import assert from 'node:assert/strict';
7import fs from 'fs';
8
9const read = (p) => fs.readFileSync(new URL(p, import.meta.url), 'utf8');
10const client = read('../src/assets/js/guardian.js');
11const page = read('../src/views/pages/guardian.ejs');
12const route = read('../src/routes/guardian.js');
13
14test('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
24test('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
30test('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]41test('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
53test('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]67test('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});
Note: See TracBrowser for help on using the repository browser.