teach/test/coloring.test.js
Ramon 1e0f0c6d03
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
feat: verfijn grafische kwaliteit kleurplaten (v0.4.68-beta)
Gewijzigde functionaliteiten:

- Brengt een rustige lijnhiërarchie aan met subtiele buitenkaders, stevige kleurvlakken en lichtere binnenlijnen.
- Voegt herkenbare textuurdetails toe aan onder meer vlinder, herfstboom, taart, zeedieren, molen, raket en kasteel.
- Verbetert het fietsframe in de duurzame-stadplaat en markeert kleurvlakken bij aanwijzen met de verfemmer.
- Borgt het consistente SVG-profiel voor alle 28 kleurplaten met aanvullende tests.

Tests: 201 geslaagd (npm test); 5 gerichte kleurplaattests geslaagd.
2026-07-29 08:37:32 +02:00

142 lines
8.1 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 bouwt kleur transparant op',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-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-region\[data-level="3"\]\{stroke-width:5\.15\}/);
assert.match(css,/\.coloring-detail\{stroke:#41516a;stroke-width:3\.6;stroke-opacity:\.9;pointer-events:none\}/);
assert.match(css,/\[data-fillable\]:hover\{stroke:#d56a1f;stroke-opacity:1\}/);
});
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'/);
});