| 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.
|
|---|
| 3 | import { test } from 'node:test';
|
|---|
| 4 | import assert from 'node:assert/strict';
|
|---|
| 5 | import fs from 'fs';
|
|---|
| 6 |
|
|---|
| 7 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 8 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 9 |
|
|---|
| 10 | const dbMod = await import('../src/config/database.js');
|
|---|
| 11 | dbMod.initializeDatabase();
|
|---|
| 12 | const T = (await import('../src/services/ThemeService.js')).default;
|
|---|
| 13 |
|
|---|
| 14 | // ── Het pakket ────────────────────────────────────────────────────
|
|---|
| 15 |
|
|---|
| 16 | test('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 |
|
|---|
| 24 | test('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 |
|
|---|
| 45 | test('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 |
|
|---|
| 57 | test('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 |
|
|---|
| 70 | const metZijbalk = { slug: 'me', layout: 'sidebar' };
|
|---|
| 71 | const zonder = { slug: 'me', layout: null };
|
|---|
| 72 |
|
|---|
| 73 | test('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 |
|
|---|
| 79 | test('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 |
|
|---|
| 86 | test('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 |
|
|---|
| 93 | test('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 | });
|
|---|