teach/test/dot-grid.test.js
Ramon fcb117eb54
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 50s
fix: lijn het stippenpatroon uit binnen elk blokformaat (v0.4.64-beta)
Gewijzigde functionaliteiten:

- De open stippen in de doelvakken stonden links-boven in hun rastercel in
  plaats van gecentreerd, waardoor het patroon scheef oogde; ze staan nu op
  dezelfde as als de gevulde stippen.

- De stippen van de geldblokken en van alle cijferblokken in een smalgemaakte
  widget staken onder het blok uit. De stipmaat komt nu uit een variabele die
  elk bloktype naast zijn eigen hoogte zet, in plaats van uit losse
  hardgecodeerde regels per widget.

- De smalle-widget-weergave verkleint alleen nog de breedte; blokhoogtes
  blijven gelijk, want juist het verkleinen van de hoogte duwde het raster naar
  buiten. Letterblokken krijgen daar een passend kleiner cijfer/letterformaat.

- Nieuw vangnet: het raster houdt altijd zijn maat en het label geeft mee als
  een blokformaat krap is, zodat een nieuw blokformaat dit niet opnieuw kan
  veroorzaken.

Tests: 196 geslaagd (4 nieuw in test/dot-grid.test.js; die vier vallen ook
daadwerkelijk om op de oude CSS). Daarnaast in de browser opgemeten voor alle
zeven blokcontexten, bij normale en smalle widgetbreedte, plus screenshots en
een controle in de echte leerlingweergave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126hjBPD2ggZ2LEoDYZSW7S
2026-07-26 08:13:02 +02:00

106 lines
5 KiB
JavaScript

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`);
});
});