source: Klonkt/test/layout-themes.test.js@ 495b019

Last change on this file since 495b019 was 495b019, checked in by Bart <bart@…>, 3 days ago

Lay-outthema's: een pakket, de beheerder kiest, en de zijbalk als eerste

Een lay-out is nu een pakket: een map in src/themes/ met een theme.json (id,
naam, versie, contract, stylesheet) en de stylesheet zelf. De zijbalk is het
eerste ingebouwde thema; een thema van buiten installeren komt later en gaat
door dezelfde lader, zodat er één vorm is.

Dezelfde markup, een ander stelsel: een thema verandert geen template. Het
contract (1) is de afspraak over de ids en klassen waar het op mag bouwen; een
thema op een ander contract valt terug op de gewone kop in plaats van half te
werken. Elke regel staat onder html[data-layout="<id>"], en een toets bewaakt
dat, zodat een thema niets raakt zolang het niet gekozen is.

WIE KIEST: de beheerder, in het sitebeheer (sites.layout). Een bezoeker niet
-- de server zet data-layout zelf, het bezoekersscript met localStorage is
weg, en ?layout= werkt alleen als voorproefje voor wie de site beheert. Het
beheer houdt altijd de gewone kop, ook bij navigeren via htmx (de lay-out reist
mee in pcmsNav): een kapot thema mag je nooit buitensluiten van de instelling
waarmee je het uitzet.

Geserveerd op /themes/<id>/<versie>/<bestand>, met de versie in het pad als
cache-buster; alleen wat het pakket zelf noemt komt eruit. De actor noemt het
gekozen thema als shaer:theme, zodat Shaer het van zijn eigen site kan halen
in plaats van er zelf een te kiezen.

Co-Authored-By: Claude Opus 5.5 <claude@…>

  • Property mode set to 100644
File size: 5.4 KB
Line 
1// Lay-outthema's (experimental/theme): een thema is een pakket, de beheerder
2// kiest, een bezoeker niet, en het beheer houdt altijd de gewone kop.
3import { test } from 'node:test';
4import assert from 'node:assert/strict';
5import fs from 'fs';
6
7process.env.DATABASE_PATH = ':memory:';
8process.env.PUBLIC_BASE_URL = 'https://test.example';
9
10const dbMod = await import('../src/config/database.js');
11dbMod.initializeDatabase();
12const T = (await import('../src/services/ThemeService.js')).default;
13
14// ── Het pakket ────────────────────────────────────────────────────
15
16test('de zijbalk is een ingebouwd thema met het huidige contract', () => {
17 const s = T.getLayout('sidebar');
18 assert.ok(s, 'het pakket wordt gevonden en klopt');
19 assert.equal(s.contract, T.LAYOUT_CONTRACT);
20 assert.match(s.version, /^\d+\.\d+\.\d+$/);
21 assert.equal(s.stylesheet, 'layout.css');
22});
23
24test('elke regel van een ingebouwd thema staat onder zijn eigen data-layout', () => {
25 // Zo raakt een thema niets zolang het niet gekozen is. Voor ingebouwde
26 // thema's staat die afbakening er met de hand; deze toets bewaakt hem.
27 for (const m of T.listLayouts()) {
28 const css = fs.readFileSync(T.layoutFile(m.id, m.version, m.stylesheet), 'utf8')
29 .replace(/\/\*[\s\S]*?\*\//g, '')
30 // De kop van een @media-blok is geen selector; de regels erin wel.
31 .replace(/@[^{;]+\{/g, '');
32 const selectors = [...css.matchAll(/([^{}@;]+)\{/g)]
33 .map((x) => x[1].trim())
34 .filter((sel) => sel && !sel.startsWith('@') && !/^\(/.test(sel));
35 assert.ok(selectors.length > 0, `${m.id} heeft regels`);
36 for (const group of selectors) {
37 for (const sel of group.split(',').map((x) => x.trim()).filter(Boolean)) {
38 assert.ok(sel.startsWith(`html[data-layout="${m.id}"]`),
39 `${m.id}: "${sel}" staat niet onder html[data-layout="${m.id}"]`);
40 }
41 }
42 }
43});
44
45test('een manifest dat niet klopt wordt geweigerd, met een reden', () => {
46 const goed = { id: 'x', name: 'X', version: '1.0.0', contract: T.LAYOUT_CONTRACT, stylesheet: 'a.css' };
47 assert.ok(T.validateLayoutManifest(goed, 'x'));
48 assert.equal(T.validateLayoutManifest({ ...goed, id: '../x' }, '../x'), null, 'geen pad als id');
49 assert.equal(T.validateLayoutManifest({ ...goed, contract: 99 }, 'x'), null, 'ander contract valt terug');
50 assert.equal(T.validateLayoutManifest({ ...goed, version: 'latest' }, 'x'), null, 'versie is x.y.z');
51 assert.equal(T.validateLayoutManifest({ ...goed, stylesheet: '../../etc/passwd' }, 'x'), null, 'stylesheet is een bestandsnaam');
52 assert.equal(T.validateLayoutManifest(goed, 'y'), null, 'id hoort bij zijn map');
53});
54
55// ── Het serveren ──────────────────────────────────────────────────
56
57test('alleen wat het pakket noemt wordt geserveerd, en alleen van deze versie', () => {
58 const s = T.getLayout('sidebar');
59 assert.ok(T.layoutFile('sidebar', s.version, 'layout.css'));
60 assert.ok(T.layoutFile('sidebar', s.version, 'theme.json'));
61 assert.equal(T.layoutFile('sidebar', s.version, '../../config/database.js'), null);
62 assert.equal(T.layoutFile('sidebar', s.version, 'anders.css'), null);
63 assert.equal(T.layoutFile('sidebar', '0.0.1', 'layout.css'), null, 'een oude versie bestaat niet meer');
64 assert.equal(T.layoutFile('..', s.version, 'layout.css'), null);
65 assert.equal(T.layoutStylesheetUrl(s), `/themes/sidebar/${s.version}/layout.css`);
66});
67
68// ── Wie kiest ─────────────────────────────────────────────────────
69
70const metZijbalk = { slug: 'me', layout: 'sidebar' };
71const zonder = { slug: 'me', layout: null };
72
73test('de site kiest; zonder keuze is het de gewone kop', () => {
74 assert.equal(T.resolveLayout(metZijbalk).id, 'sidebar');
75 assert.equal(T.resolveLayout(zonder), null);
76 assert.equal(T.resolveLayout({ slug: 'me', layout: 'bestaat-niet' }), null, 'een onbekend thema valt terug');
77});
78
79test('een bezoeker kiest niet: ?layout= doet alleen iets voor wie de site beheert', () => {
80 assert.equal(T.resolveLayout(zonder, { preview: 'sidebar' }), null, 'bezoeker');
81 assert.equal(T.resolveLayout(zonder, { preview: 'sidebar', canManage: true }).id, 'sidebar', 'beheerder');
82 assert.equal(T.resolveLayout(metZijbalk, { preview: 'top', canManage: true }), null, 'beheerder kan de kop voorproeven');
83 assert.equal(T.resolveLayout(metZijbalk, { preview: 'top' }).id, 'sidebar', 'een bezoeker zet hem niet uit');
84});
85
86test('het beheer houdt altijd de gewone kop, ook met een thema gekozen', () => {
87 assert.equal(T.resolveLayout(metZijbalk, { isAdminPage: true }), null);
88 assert.equal(T.resolveLayout(zonder, { isAdminPage: true, canManage: true, preview: 'sidebar' }), null);
89});
90
91// ── Wat Shaer ervan te zien krijgt ────────────────────────────────
92
93test('de actor noemt het thema alleen als er een gekozen is', () => {
94 assert.deepEqual(T.layoutActorProp('https://test.example', zonder), {});
95 const p = T.layoutActorProp('https://test.example', metZijbalk)['shaer:theme'];
96 assert.equal(p.type, 'shaer:Theme');
97 assert.equal(p.id, `https://test.example/themes/sidebar/${T.getLayout('sidebar').version}/theme.json`);
98 assert.equal(p['shaer:contract'], T.LAYOUT_CONTRACT);
99});
Note: See TracBrowser for help on using the repository browser.