All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 34s
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)
151 lines
9.2 KiB
JavaScript
151 lines
9.2 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,
|
||
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 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}`));
|
||
}
|
||
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'/);
|
||
});
|