// Lay-outthema's (experimental/theme): een thema is een pakket, de beheerder // kiest, een bezoeker niet, en het beheer houdt altijd de gewone kop. import { test } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'fs'; process.env.DATABASE_PATH = ':memory:'; process.env.PUBLIC_BASE_URL = 'https://test.example'; const dbMod = await import('../src/config/database.js'); dbMod.initializeDatabase(); const T = (await import('../src/services/ThemeService.js')).default; // ── Het pakket ──────────────────────────────────────────────────── test('de zijbalk is een ingebouwd thema met het huidige contract', () => { const s = T.getLayout('sidebar'); assert.ok(s, 'het pakket wordt gevonden en klopt'); assert.equal(s.contract, T.LAYOUT_CONTRACT); assert.match(s.version, /^\d+\.\d+\.\d+$/); assert.equal(s.stylesheet, 'layout.css'); }); test('elke regel van een ingebouwd thema staat onder zijn eigen data-layout', () => { // Zo raakt een thema niets zolang het niet gekozen is. Voor ingebouwde // thema's staat die afbakening er met de hand; deze toets bewaakt hem. for (const m of T.listLayouts()) { const css = fs.readFileSync(T.layoutFile(m.id, m.version, m.stylesheet), 'utf8') .replace(/\/\*[\s\S]*?\*\//g, '') // De kop van een @media-blok is geen selector; de regels erin wel. .replace(/@[^{;]+\{/g, ''); const selectors = [...css.matchAll(/([^{}@;]+)\{/g)] .map((x) => x[1].trim()) .filter((sel) => sel && !sel.startsWith('@') && !/^\(/.test(sel)); assert.ok(selectors.length > 0, `${m.id} heeft regels`); for (const group of selectors) { for (const sel of group.split(',').map((x) => x.trim()).filter(Boolean)) { assert.ok(sel.startsWith(`html[data-layout="${m.id}"]`), `${m.id}: "${sel}" staat niet onder html[data-layout="${m.id}"]`); } } } }); test('een manifest dat niet klopt wordt geweigerd, met een reden', () => { const goed = { id: 'x', name: 'X', version: '1.0.0', contract: T.LAYOUT_CONTRACT, stylesheet: 'a.css' }; assert.ok(T.validateLayoutManifest(goed, 'x')); assert.equal(T.validateLayoutManifest({ ...goed, id: '../x' }, '../x'), null, 'geen pad als id'); assert.equal(T.validateLayoutManifest({ ...goed, contract: 99 }, 'x'), null, 'ander contract valt terug'); assert.equal(T.validateLayoutManifest({ ...goed, version: 'latest' }, 'x'), null, 'versie is x.y.z'); assert.equal(T.validateLayoutManifest({ ...goed, stylesheet: '../../etc/passwd' }, 'x'), null, 'stylesheet is een bestandsnaam'); assert.equal(T.validateLayoutManifest(goed, 'y'), null, 'id hoort bij zijn map'); }); // ── Het serveren ────────────────────────────────────────────────── test('alleen wat het pakket noemt wordt geserveerd, en alleen van deze versie', () => { const s = T.getLayout('sidebar'); assert.ok(T.layoutFile('sidebar', s.version, 'layout.css')); assert.ok(T.layoutFile('sidebar', s.version, 'theme.json')); assert.equal(T.layoutFile('sidebar', s.version, '../../config/database.js'), null); assert.equal(T.layoutFile('sidebar', s.version, 'anders.css'), null); assert.equal(T.layoutFile('sidebar', '0.0.1', 'layout.css'), null, 'een oude versie bestaat niet meer'); assert.equal(T.layoutFile('..', s.version, 'layout.css'), null); assert.equal(T.layoutStylesheetUrl(s), `/themes/sidebar/${s.version}/layout.css`); }); // ── Wie kiest ───────────────────────────────────────────────────── const metZijbalk = { slug: 'me', layout: 'sidebar' }; const zonder = { slug: 'me', layout: null }; test('de site kiest; zonder keuze is het de gewone kop', () => { assert.equal(T.resolveLayout(metZijbalk).id, 'sidebar'); assert.equal(T.resolveLayout(zonder), null); assert.equal(T.resolveLayout({ slug: 'me', layout: 'bestaat-niet' }), null, 'een onbekend thema valt terug'); }); test('een bezoeker kiest niet: ?layout= doet alleen iets voor wie de site beheert', () => { assert.equal(T.resolveLayout(zonder, { preview: 'sidebar' }), null, 'bezoeker'); assert.equal(T.resolveLayout(zonder, { preview: 'sidebar', canManage: true }).id, 'sidebar', 'beheerder'); assert.equal(T.resolveLayout(metZijbalk, { preview: 'top', canManage: true }), null, 'beheerder kan de kop voorproeven'); assert.equal(T.resolveLayout(metZijbalk, { preview: 'top' }).id, 'sidebar', 'een bezoeker zet hem niet uit'); }); test('het beheer houdt altijd de gewone kop, ook met een thema gekozen', () => { assert.equal(T.resolveLayout(metZijbalk, { isAdminPage: true }), null); assert.equal(T.resolveLayout(zonder, { isAdminPage: true, canManage: true, preview: 'sidebar' }), null); }); // ── Wat Shaer ervan te zien krijgt ──────────────────────────────── test('de actor noemt het thema alleen als er een gekozen is', () => { assert.deepEqual(T.layoutActorProp('https://test.example', zonder), {}); const p = T.layoutActorProp('https://test.example', metZijbalk)['shaer:theme']; assert.equal(p.type, 'shaer:Theme'); assert.equal(p.id, `https://test.example/themes/sidebar/${T.getLayout('sidebar').version}/theme.json`); assert.equal(p['shaer:contract'], T.LAYOUT_CONTRACT); });