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 files=[ 'public/js/widgets/coloring-scenes-junior.js','public/js/widgets/coloring-scenes-middle.js', 'public/js/widgets/coloring-scenes-senior.js','public/js/widgets/coloring-scenes-advanced.js', 'public/js/widgets/coloring.js' ]; const source=(await Promise.all(files.map(file=>readFile(file,'utf8')))).join('\n'); 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') ]); for(const name of ['junior','middle','senior','advanced']){ assert.ok(html.indexOf(`js/widgets/coloring-scenes-${name}.js`)>0); assert.ok(html.indexOf(`js/widgets/coloring-scenes-${name}.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 figures=(page.svg.match(/class="coloring-figure"/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>=10,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>=1,page.id+' heeft herkenbaar, begrensd binnenlijnwerk'); assert.ok(figures>=1,page.id+' groepeert onderdelen tot samenhangende figuren'); 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:['butterfly-forewing-left','butterfly-abdomen'], farmyard:['cow-silhouette','cow-muzzle'], 'forest-picnic':['rabbit-silhouette','rabbit-hind-leg'], ocean:['fish-silhouette','turtle-shell','turtle-head'], garden:['snail-head-neck','ladybird-body'], 'dinosaur-valley':['dinosaur-silhouette'], 'spring-nest':['bird-silhouette','bird-wing'], 'bee-meadow':['bee-abdomen','bee-thorax','bee-head'], rainforest:['macaw-silhouette','macaw-wing','macaw-upper-beak'], 'coral-reef':['clownfish-body','seahorse-silhouette'], ecosystem:['frog-silhouette','frog-head-profile','frog-near-hind-leg'], 'savanna-web':['lion-body','lion-face','zebra-body','zebra-head'] }; 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-silhouette','cow-muzzle','cow-ear-back','cow-horn-front'], 'forest-picnic':['rabbit-tail','rabbit-ear-back','rabbit-ear-front','rabbit-silhouette','rabbit-hind-leg','rabbit-forepaw'], ocean:['fish-tail','fish-dorsal-fin','fish-silhouette','fish-gill-cover','turtle-rear-flipper','turtle-front-flipper-near','turtle-shell','turtle-neck','turtle-head'], garden:['ladybird-leg-back-a','ladybird-body','ladybird-pronotum','ladybird-wing-left','ladybird-spot-left-one'], 'dinosaur-valley':['dinosaur-leg-0','dinosaur-silhouette','dinosaur-belly'], 'spring-nest':['bird-tail-top','bird-silhouette','bird-wing','bird-beak'], 'bee-meadow':['bee-hindwing','bee-forewing','bee-abdomen','bee-stripe-one','bee-thorax','bee-head','bee-compound-eye','bee-mandible'], rainforest:['macaw-tail-long','macaw-tail-short','macaw-silhouette','macaw-wing','macaw-face-patch','macaw-upper-beak','macaw-lower-beak'], 'coral-reef':['clownfish-tail','clownfish-dorsal-fin','clownfish-body','clownfish-head-band','seahorse-fin','seahorse-silhouette','seahorse-belly','seahorse-crown'], ecosystem:['frog-far-hind-leg','frog-far-foot','frog-near-hind-leg','frog-near-foot','frog-silhouette','frog-head-profile','frog-visible-eye','frog-tympanum'], 'savanna-web':['lion-tail','lion-leg-0','lion-body','lion-mane','lion-face','zebra-tail','zebra-leg-0','zebra-body','zebra-neck','zebra-head','zebra-muzzle','zebra-mane'], rocket:['rocket-exhaust-outer','rocket-fin-left','rocket-body','rocket-window'], 'fire-station':['engine-chassis','engine-body','engine-cab','engine-ladder','engine-wheel-left','engine-hub-left'], 'robot-workshop':['robot-leg-left','robot-upper-arm-left','robot-forearm-left','robot-body','robot-neck','robot-head'], windmill:['mill-base','mill-cap','mill-sail-north','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 de contourlaag bewaart buiten- en anatomische binnenlijnen',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.match(css,/mix-blend-mode:multiply/); assert.match(source,/class="coloring-art"><\/div>]+><\/canvas>