import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; /* Stippenraster (braillepatroon uit dotGrid(), data.js). Het ging mis doordat de stipmaat per bloktype hardgecodeerd was terwijl blokhoogte, labelhoogte en marges óók per type verschillen: de geldblokken erfden de maat van een ander formaat en de smalle-widget-query verkleinde alleen de blokken. Gevolg: de stippen staken onder het blok uit. Deze test bewaakt de regels die dat voorkomen; de volledige geometrie is daarnaast in de browser opgemeten. */ const CSS = await readFile('public/css/teach.css', 'utf8'); /* alle regels als [selector, declaraties]; commentaar eruit want daar staan voorbeelden in die anders als declaratie meetellen */ function rules(css) { const clean = css.replace(/\/\*[\s\S]*?\*\//g, ''); const out = []; for (const chunk of clean.split('}')) { const i = chunk.lastIndexOf('{'); if (i < 0) continue; const selector = chunk.slice(0, i).split('{').pop().trim(); if (!selector || selector.startsWith('@')) continue; out.push([selector, chunk.slice(i + 1)]); } return out; } const RULES = rules(CSS); const declares = (decls, prop) => new RegExp(`(^|;|\\s)${prop}\\s*:`).test(decls); /* het bloktype in een selector: .money-digit-block, .blok.sm, .blok, ... */ function contextTokens(selector) { return selector.split(',') .map((part) => part.trim()) .filter((part) => /\.(blok|slot)\b/.test(part) && !/\s\.(bl|ghost|dots|blok-dots|slot-dots)\b/.test(part)) .map((part) => (part.match(/\.[a-z0-9-]*(?:block|slot|blok)\b/g) || []).join('')); } test('elk bloktype dat een eigen hoogte krijgt, zet ook zijn eigen stipmaat', () => { const withDot = new Set(); RULES.forEach(([selector, decls]) => { if (declares(decls, '--dot')) contextTokens(selector).forEach((t) => withDot.add(t)); }); const missing = []; RULES.forEach(([selector, decls]) => { if (!declares(decls, 'height')) return; contextTokens(selector).forEach((token) => { if (!token) return; // een variant erft de maat van zijn basis (.blok/.slot) als die het zet const base = token.includes('slot') ? '.slot' : '.blok'; if (!withDot.has(token) && !withDot.has(base)) missing.push(`${selector} (${token})`); }); }); assert.deepEqual(missing, [], 'deze regels zetten een blokhoogte zonder dat er ergens een --dot voor dat type bestaat'); assert.ok(withDot.has('.blok') && withDot.has('.slot'), 'basisblok en -slot zetten --dot'); }); test('de stippen zijn nooit kleiner dan 6px: het patroon moet leesbaar blijven', () => { const values = [...CSS.matchAll(/--dot\s*:\s*([\d.]+)px/g)].map((m) => Number(m[1])); assert.ok(values.length >= 5, `verwacht meerdere stipmaten, gevonden: ${values.length}`); values.forEach((v) => assert.ok(v >= 6, `stipmaat ${v}px is onder de leesbaarheidsgrens`)); }); test('geen enkel bloktype heeft nog een eigen hardgecodeerd stippenraster', () => { const hardcoded = RULES.filter(([selector, decls]) => /\.dots\b/.test(selector) && /(grid-template-(columns|rows)\s*:[^;]*\d+px|(?:^|;|\s)(width|height)\s*:\s*\d+px)/.test(decls)); assert.deepEqual(hardcoded.map(([s]) => s), [], 'stipmaten horen via var(--dot) te lopen, anders loopt een nieuw blokformaat weer uit de pas'); }); /* Narekening van de verticale begroting per bloktype. De blokrand telt mee (border-box), dus: rand + padding-top + labelhoogte + rasterhoogte + ondermarge moet binnen de blokhoogte passen. Rasterhoogte = 3 stippen + 2 tussenruimtes van --dot/3. */ const CONTEXTS = [ { naam: 'letterblok', maat: '.blok', label: '.blok .bl', marge: '.blok .blok-dots', dot: '.blok' }, { naam: 'letterslot', maat: '.slot', label: '.slot .ghost', marge: '.slot .slot-dots', dot: '.slot' }, { naam: 'rekenblok', maat: '.blok.sm', label: '.blok.sm .bl', marge: '.blok.sm .blok-dots', dot: '.blok.sm' }, { naam: 'rekenslot', maat: '.slot.sm', label: '.slot.sm .ghost', marge: '.slot.sm .slot-dots', dot: '.slot.sm' }, ]; function value(selectorPart, prop) { let found = null; RULES.forEach(([selector, decls]) => { if (!selector.split(',').some((part) => part.trim() === selectorPart)) return; const m = decls.match(new RegExp(`(?:^|;|\\s)${prop}\\s*:\\s*([\\d.]+)px`)); if (m) found = Number(m[1]); }); return found; } test('label, stippenraster en marges passen samen binnen elk basisblok', () => { CONTEXTS.forEach((ctx) => { const hoogte = value(ctx.maat, 'height'); const padding = value(ctx.maat, 'padding-top') ?? 0; const rand = (value(ctx.maat, 'border') ?? 2.5) * 2; const label = value(ctx.label, 'height'); const marge = value(ctx.marge, 'margin-bottom'); const dot = value(ctx.dot, '--dot'); assert.ok(hoogte && label && marge && dot, `${ctx.naam}: maten niet gevonden`); const raster = 3 * dot + 2 * (dot / 3); const nodig = padding + label + raster + marge + rand; assert.ok(nodig <= hoogte, `${ctx.naam}: ${nodig.toFixed(1)}px nodig in een blok van ${hoogte}px - de stippen steken eruit`); }); });