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, brushWidth:coloringBrushWidth, 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')0); assert.ok(html.indexOf('js/widgets/coloring.js'){ 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(/]*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,/item.id===id); names.forEach(name=>assert.ok(page.svg.includes(`data-part="${name}"`),`${id} mist ${name}`)); } const organicContours={ butterfly:['wing-left-top','body'],ocean:['fish-body','turtle-shell'],garden:['snail-body','ladybird'], farmyard:['cow-body','cow-head'],'forest-picnic':['rabbit-body','rabbit-head'], 'dinosaur-valley':['dino-body','dino-neck','dino-head'],'spring-nest':['bird-body','bird-head'], 'bee-meadow':['bee-body','bee-head'],'coral-reef':['clownfish-body','seahorse-body'], 'savanna-web':['lion-body','lion-mane','zebra-body','zebra-head'],rainforest:['parrot-body','parrot-head'], ecosystem:['frog-body'],rocket:['rocket-body'],'fire-station':['engine-body'] }; for(const [id,names] of Object.entries(organicContours)){ const page=pages.find(item=>item.id===id); names.forEach(name=>{ const match=page.svg.match(new RegExp(`]*data-part="${name}"[^>]*d="([^"]+)"`)); assert.ok(match,`${id}/${name} gebruikt een getekend contoursilhouet`); assert.ok((match[1].match(/C/g)||[]).length>=2,`${id}/${name} heeft vloeiende Bézierovergangen`); }); } const paintOrder={ farmyard:['cow-tail','cow-leg-0','cow-body','cow-head','cow-snout'], 'forest-picnic':['rabbit-tail','rabbit-ear-a','rabbit-body','rabbit-hind-leg','rabbit-head'], 'dinosaur-valley':['dino-tail','dino-leg-0','dino-plate-0','dino-body','dino-neck','dino-head'], 'spring-nest':['bird-tail-a','bird-body','bird-wing','bird-head','bird-beak'], ocean:['fish-tail','fish-top-fin','fish-body','turtle-tail','flipper-0','turtle-shell','turtle-head'], 'bee-meadow':['bee-wing-a','bee-body','bee-stripe-a','bee-head'], 'coral-reef':['clownfish-tail','clownfish-fin-top','clownfish-body','seahorse-fin','seahorse-body'], 'savanna-web':['lion-tail','lion-leg-0','lion-body','lion-mane','zebra-tail','zebra-leg-0','zebra-body','zebra-neck','zebra-head'], rainforest:['parrot-branch','parrot-tail','parrot-body','parrot-wing','parrot-head','parrot-beak'], ecosystem:['frog-leg-a','frog-body','frog-eye-a'], rocket:['flame-outer','rocket-left-fin','rocket-body','rocket-window'], 'fire-station':['engine-wheel-a','engine-body','engine-cab','engine-hub-a'], 'robot-workshop':['robot-arm-a','robot-leg-a','robot-neck','robot-head','robot-body'], windmill:['mill-body','mill-roof','sail-0','mill-hub'] }; for(const [id,names] of Object.entries(paintOrder)){ const page=pages.find(item=>item.id===id),positions=names.map(name=>page.svg.indexOf(`data-part="${name}"`)); assert.ok(positions.every(position=>position>=0),`${id} bevat alle aansluitende onderdelen`); assert.deepEqual([...positions].sort((a,b)=>a-b),positions,`${id} schildert appendages achter het hoofdsilhouet`); } assert.equal(api.pagesFor(5,'science').length,2); assert.equal(api.pagesFor(3,'science').length,1); }); test('kwast reageert op pendruk en transparante inktlagen houden lijnen zichtbaar',async()=>{ const {source,api}=await coloringContext(); assert.ok(api.brushWidth(7,.9,'pen')>api.brushWidth(7,.2,'pen')); assert.equal(api.brushWidth(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:.72','quadraticCurveTo'])assert.ok(source.includes(feature),feature); assert.match(source,/const savedTool=initState&&initState\.tool==="pencil"\?"brush"/); assert.match(source,/\["brush","🖌️","colorToolBrush"\]/); const css=await readFile('public/css/coloring.css','utf8'); assert.doesNotMatch(css,/mix-blend-mode/); assert.match(source,/class="coloring-art"><\/div>]+><\/canvas>