teach/test/coloring.test.js
Ramon 979f30a935
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
feat: geef kleurplaten een grafische upgrade (v0.4.69-beta)
Gewijzigde functionaliteiten:
- Splits kleurplaten in kleur-, potlood- en bovenste inktlaag zodat contouren zichtbaar blijven.
- Versterk lijnen met een lichte contrastrand en donkere inkt, met behoud van de juiste overlappingen.
- Vernieuw werkvlak, gereedschappen en kleurenpalet voor een krachtiger grafische uitstraling.
- Voeg regressietests toe voor laagvolgorde en inktweergave.

Tests:
- npm test (201 geslaagd)
2026-07-29 10:18:59 +02:00

151 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import test from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import {runInNewContext} from 'node:vm';
async function coloringContext(){
const [extra,widget]=await Promise.all([
readFile('public/js/widgets/coloring-scenes-extra.js','utf8'),readFile('public/js/widgets/coloring.js','utf8')
]),source=extra+'\n'+widget;
const context={};
runInNewContext(source+`;
globalThis.__coloring={
pages:COLORING_PAGES,
themes:COLORING_THEME_KEYS,
pagesFor:coloringPagesFor,
pencilWidth:coloringPencilWidth,
safeColor:coloringSafeColor
}`,context);
return{source,api:context.__coloring};
}
test('kleurplatenwidget laadt vóór het bord en is als creatieve leerlingwidget geregistreerd',async()=>{
const [html,board,pupil,cast,core,css]=await Promise.all([
readFile('public/index.html','utf8'),readFile('public/js/board.js','utf8'),
readFile('public/js/pupil.js','utf8'),readFile('public/js/cast.js','utf8'),
readFile('public/js/core.js','utf8'),readFile('public/css/coloring.css','utf8')
]);
assert.ok(html.indexOf('js/widgets/coloring-scenes-extra.js')>0);
assert.ok(html.indexOf('js/widgets/coloring-scenes-extra.js')<html.indexOf('js/widgets/coloring.js'));
assert.ok(html.indexOf('js/widgets/coloring.js')>0);
assert.ok(html.indexOf('js/widgets/coloring.js')<html.indexOf('js/board.js'));
assert.match(html,/css\/coloring\.css\?v=__V__/);
assert.match(board,/id:"coloring"[^\n]*cat:"creatief"[^\n]*mount:mountColoring/);
assert.match(pupil,/PUPIL_WIDGET_CATS\s*=\s*\[[^\]]*"creatief"/);
assert.match(cast,/CAST_WIDGET_CATEGORIES=new Set\(\[[^\]]*"creatief"/);
assert.match(css,/\.coloring-paper[^}]*aspect-ratio:4\/3/);
for(const key of ['wg_coloring','wg_coloring_d','catCreative','colorToolPencil','colorToolBucket','colorBucketHint','colorPencilHint']){
assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
}
});
test('elk leerjaar heeft zeven herkenbare kleurplaten met diverse themas en drie detailniveaus',async()=>{
const {api}=await coloringContext();
const pages=JSON.parse(JSON.stringify(api.pages));
assert.equal(pages.length,28);
assert.equal(new Set(pages.map(page=>page.id)).size,pages.length);
const added=[
'farmyard','rainbow-weather','construction-site','forest-picnic',
'dinosaur-valley','spring-nest','fire-station','bee-meadow',
'coral-reef','mountain-camp','robot-workshop','canal-street',
'solar-observatory','savanna-web','japanese-garden'
];
assert.equal(added.filter(id=>pages.some(page=>page.id===id)).length,15);
for(let year=1;year<=8;year++){
const suitable=JSON.parse(JSON.stringify(api.pagesFor(year)));
assert.equal(suitable.length,7,`groep ${year} heeft zeven kleurplaten`);
assert.ok(suitable.every(page=>year>=page.years[0]&&year<=page.years[1]));
assert.equal(new Set(suitable.map(page=>page.theme)).size,5,`groep ${year} heeft alle vijf thema's`);
}
for(const page of pages){
assert.ok(api.themes[page.theme],page.id+' heeft een bekend thema');
assert.ok(page.title.nl&&page.title.en,page.id+' heeft een tweetalige titel');
const parts=[...page.svg.matchAll(/data-part="([^"]+)"/g)].map(match=>match[1]);
const details=(page.svg.match(/class="coloring-detail"/g)||[]).length;
const openFillablePaths=[...page.svg.matchAll(/<path\b[^>]*data-fillable="true"[^>]*data-part="([^"]+)"[^>]* d="([^"]+)"/g)]
.filter(match=>!/[zZ]\s*$/.test(match[2])).map(match=>match[1]);
assert.ok(parts.length>=14,page.id+' heeft voldoende afzonderlijke kleurvakken');
assert.equal(new Set(parts).size,parts.length,page.id+' heeft unieke kleurvakken');
assert.deepEqual(openFillablePaths,[],page.id+' heeft alleen gesloten aanklikbare paden');
assert.ok(details>=4,page.id+' heeft voldoende herkenbaar binnenlijnwerk');
assert.match(page.svg,/class="coloring-sheet"[^>]*preserveAspectRatio="xMidYMid meet"/);
assert.match(page.svg,/data-role="backdrop"/,page.id+' heeft een subtiel buitenkader');
assert.match(page.svg,new RegExp(`data-scene="${page.id}"`));
assert.match(page.svg,/data-level="2"/);
assert.match(page.svg,/data-level="3"/);
assert.doesNotMatch(page.svg,/<script|on\w+=/i);
}
const landmarks={
farmyard:['barn','cow-body','cow-head','cow-horn-left','cow-tail'],
'rainbow-weather':['umbrella-outer-left','umbrella-inner-right','boot-left'],
'construction-site':['digger-body','bucket','bucket-tooth-a'],
ocean:['fish-body','turtle-shell','turtle-tail'],
rainforest:['forest-canopy-left','parrot-body','parrot-branch'],
'dinosaur-valley':['volcano','dino-body','dino-head','dino-tail'],
'fire-station':['engine-body','engine-ladder','engine-hose-reel'],
'coral-reef':['clownfish-body','coral-red','anemone-base','reef-starfish'],
'savanna-web':['lion-mane','zebra-neck','zebra-muzzle','zebra-leg-0'],
'robot-workshop':['robot-head','robot-body','robot-arm-a','gear-a'],
'japanese-garden':['pagoda-base','bridge','cherry-trunk','koi-a']
};
for(const [id,names] of Object.entries(landmarks)){
const page=pages.find(item=>item.id===id);
names.forEach(name=>assert.ok(page.svg.includes(`data-part="${name}"`),`${id} mist ${name}`));
}
assert.equal(api.pagesFor(5,'science').length,2);
assert.equal(api.pagesFor(3,'science').length,1);
});
test('potlood reageert op echte pendruk en de bovenste inktlaag houdt lijnen zichtbaar',async()=>{
const {source,api}=await coloringContext();
assert.ok(api.pencilWidth(7,.9,'pen')>api.pencilWidth(7,.2,'pen'));
assert.equal(api.pencilWidth(7,0,'mouse'),7);
assert.equal(api.safeColor('#AABBCC'),'#aabbcc');
assert.equal(api.safeColor('red'),'#e0554d');
for(const feature of ['getCoalescedEvents','event.pressure','erase?1:.27','quadraticCurveTo','Math.random()'])assert.ok(source.includes(feature),feature);
const css=await readFile('public/css/coloring.css','utf8');
assert.match(css,/mix-blend-mode:multiply/);
assert.match(source,/class="coloring-art"><\/div><canvas[^>]+><\/canvas><div class="coloring-ink" aria-hidden="true"><\/div>/);
assert.match(source,/coloringInkOverlay\(svg,"coloring-ink-halo"\)/);
assert.match(source,/coloringInkOverlay\(svg,"coloring-ink-overlay"\)/);
assert.match(source,/const overlay=svg\.cloneNode\(true\)/);
assert.match(source,/node\.removeAttribute\("data-fillable"\)/);
assert.match(source,/inkHost\.replaceChildren\(coloringInkOverlay\(svg,"coloring-ink-halo"\),coloringInkOverlay\(svg,"coloring-ink-overlay"\)\)/);
assert.match(source,/class="coloring-region" data-fillable="true"/);
assert.match(source,/class="coloring-sheet"[^>]*preserveAspectRatio="xMidYMid meet"/);
assert.match(source,/fill-rule="evenodd" clip-rule="evenodd"[^>]*stroke-width="6\.5"[^>]*stroke-miterlimit="2"/);
assert.match(css,/\.coloring-svg-lines\{shape-rendering:geometricPrecision\}/);
assert.match(css,/\.coloring-region\[data-role="backdrop"\]\{stroke-width:4\.25\}/);
assert.match(css,/\.coloring-art\{z-index:1\}\.coloring-paper canvas\{z-index:2\}\.coloring-ink\{z-index:3;pointer-events:none\}/);
assert.match(css,/\.coloring-ink-halo\{mix-blend-mode:screen\}\.coloring-ink-overlay\{mix-blend-mode:multiply\}/);
assert.match(css,/\.coloring-ink-halo \.coloring-region\{fill:#000!important;stroke:#fff;stroke-width:10\.25;stroke-opacity:\.96\}/);
assert.match(css,/\.coloring-ink-overlay \.coloring-region\{fill:#fff!important;stroke:var\(--coloring-ink\);stroke-width:6\.5;stroke-opacity:1\}/);
assert.match(css,/\.coloring-ink-overlay \.coloring-detail\{fill:none!important;stroke:var\(--coloring-detail\)!important;stroke-width:3\.8;stroke-opacity:1\}/);
assert.match(css,/\.coloring-ink-overlay \.coloring-ink-dot\{fill:var\(--coloring-ink\)!important;stroke:none!important\}/);
});
test('verfemmer vult een SVG-vak in één keer en state bewaart potlood én vlakkleuren',async()=>{
const {source}=await coloringContext();
assert.match(source,/event\.target\.closest\("\[data-fillable\]\[data-part\]\"\)/);
assert.match(source,/part\.setAttribute\("fill",color\)/);
assert.match(source,/fills\[part\.dataset\.part\]=color/);
assert.match(source,/canvas\.toDataURL\("image\/png"\)/);
assert.match(source,/return\{theme,pageId,fills:\{\.\.\.fills\},ink,tool,color,size,completed,yearGroup,level/);
assert.match(source,/opts\.onProgress/);
});
test('creatief niveau is beschikbaar per klas, leerling en thuiskind',async()=>{
const [api,admin,progressions,parent,wiki,migration,live]=await Promise.all([
readFile('src/api.js','utf8'),readFile('public/js/admin.js','utf8'),
readFile('public/js/learning-progressions.js','utf8'),readFile('public/js/parent.js','utf8'),
readFile('public/js/wiki.js','utf8'),
readFile('db/024_creative_pupil_levels.sql','utf8'),readFile('src/live-classroom.js','utf8')
]);
assert.match(api,/LEVEL_SUBJECTS = \[[^\]]*'creatief'/);
assert.match(admin,/KM_SUBJECTS = \[[^\]]*"creatief"/);
assert.match(progressions,/LEARNING_SUBJECTS=\[[^\]]*"creatief"/);
assert.match(parent,/\["taal","rekenen","creatief"\]\.forEach/);
assert.match(wiki,/creatief:"parentLevelCreative"/);
assert.match(migration,/CHECK \(subject IN \('taal','rekenen','world','creatief'\)\)/);
assert.match(live,/LEVEL_SUBJECTS = new Set\(\[[^\]]*'creatief'/);
});