All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- laat de gum ook verfpotvullingen bij tikken en slepen verwijderen - vervang dubbele filterlagen door één gemaskeerde contourlaag zonder witte halo - verberg overlappende herkenningshulplijnen en dek achterliggende contouren correct af - voeg regressietests toe voor gum, vulranden en lijnlagen
200 lines
13 KiB
JavaScript
200 lines
13 KiB
JavaScript
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')<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','colorToolBrush','colorToolBucket','colorBucketHint','colorBrushHint']){
|
||
assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
|
||
}
|
||
});
|
||
|
||
test('elk leerjaar heeft zeven herkenbare kleurplaten met diverse thema’s 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}`));
|
||
}
|
||
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(`<path\\b[^>]*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 één gemaskeerde contourlaag houdt alleen buitenlijnen 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.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\)/);
|
||
assert.match(source,/const overlay=svg\.cloneNode\(true\)/);
|
||
assert.match(source,/overlay\.querySelectorAll\("\.coloring-detail,\.coloring-recognition"\)\.forEach\(node=>node\.remove\(\)\)/);
|
||
assert.match(source,/node\.removeAttribute\("data-fillable"\)/);
|
||
assert.match(source,/node\.setAttribute\("fill","#fff"\)/);
|
||
assert.match(source,/inkHost\.replaceChildren\(coloringInkOverlay\(svg\)\)/);
|
||
assert.doesNotMatch(source,/coloring-ink-halo|feMorphology|feColorMatrix/);
|
||
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="5\.6"[^>]*stroke-miterlimit="2"/);
|
||
assert.match(css,/\.coloring-svg-lines\{shape-rendering:geometricPrecision\}/);
|
||
assert.match(css,/\.coloring-region\[data-role="backdrop"\]\{stroke-width:3\.2\}/);
|
||
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-svg-lines \.coloring-recognition\{display:none\}/);
|
||
assert.match(css,/\.coloring-ink-overlay\{pointer-events:none;shape-rendering:geometricPrecision;mix-blend-mode:multiply\}/);
|
||
assert.match(css,/\.coloring-ink-svg \.coloring-region\{fill:#fff!important;stroke:var\(--coloring-ink\);stroke-width:5\.6;stroke-opacity:1\}/);
|
||
assert.doesNotMatch(css,/coloring-ink-halo|\.coloring-ink-svg \.coloring-detail/);
|
||
assert.match(css,/\.coloring-app\[data-tool="bucket"\] \.coloring-paper canvas\{pointer-events:none\}/);
|
||
assert.match(source,/canvas\.style\.pointerEvents=tool==="bucket"\?"none":"auto";art\.style\.pointerEvents=tool==="bucket"\?"auto":"none"/);
|
||
assert.match(source,/className="coloring-tool-label"/);
|
||
assert.match(source,/toolHost\.setAttribute\("role","toolbar"\)/);
|
||
assert.match(css,/\.coloring-tool-buttons button\{width:auto;min-width:72px/);
|
||
});
|
||
|
||
test('verfemmer vult een SVG-vak en de gum wist zowel kwast als opgeslagen vlakkleur',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,/document\.elementFromPoint\(event\.clientX,event\.clientY\)/);
|
||
assert.match(source,/Object\.prototype\.hasOwnProperty\.call\(fills,part\.dataset\.part\)/);
|
||
assert.match(source,/delete fills\[part\.dataset\.part\];part\.setAttribute\("fill","#fff"\)/);
|
||
assert.match(source,/if\(erase\)eraseFillAt\(event\)/);
|
||
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'/);
|
||
});
|