All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 30s
390 lines
25 KiB
JavaScript
390 lines
25 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {readFile} from 'node:fs/promises';
|
||
import {runInNewContext} from 'node:vm';
|
||
|
||
const ids=['birthday','numberline','sentence'];
|
||
const learningIds=['fractions','clock','placevalue','money','wordtypes','schedule','mood','poll'];
|
||
const worldIds=['trees','birds','plants','topography'];
|
||
|
||
test('nieuwe widgets zijn geladen vóór het widgetregister', async()=>{
|
||
const html=await readFile('public/index.html','utf8');
|
||
const board=html.indexOf('js/board.js');
|
||
for(const id of ids){
|
||
const pos=html.indexOf(`js/widgets/${id}.js`);
|
||
assert.ok(pos>0 && pos<board, `${id} moet vóór board.js laden`);
|
||
}
|
||
});
|
||
|
||
test('nieuwe widgets zijn geregistreerd en bewaren state', async()=>{
|
||
const registry=await readFile('public/js/board.js','utf8');
|
||
for(const id of ids){
|
||
assert.match(registry,new RegExp(`id:"${id}"`));
|
||
const source=await readFile(`public/js/widgets/${id}.js`,'utf8');
|
||
assert.match(source,/getState/);
|
||
assert.doesNotMatch(source,/innerHTML\s*=.*(?:name|word\.value)/);
|
||
}
|
||
});
|
||
|
||
test('nieuwe widgetnamen en beschrijvingen zijn tweetalig', async()=>{
|
||
const core=await readFile('public/js/core.js','utf8');
|
||
for(const id of ids){
|
||
assert.equal((core.match(new RegExp(`wg_${id}:`,'g'))||[]).length,2);
|
||
assert.equal((core.match(new RegExp(`wg_${id}_d:`,'g'))||[]).length,2);
|
||
}
|
||
});
|
||
|
||
|
||
test('educatieve widgetmodule laadt en registreert alle acht widgets', async()=>{
|
||
const html=await readFile('public/index.html','utf8');
|
||
assert.ok(html.indexOf('js/widgets/learning.js')<html.indexOf('js/board.js'));
|
||
const [board,source]=await Promise.all([readFile('public/js/board.js','utf8'),readFile('public/js/widgets/learning.js','utf8')]);
|
||
for(const id of learningIds){
|
||
assert.match(board,new RegExp('id:"'+id+'"'));
|
||
const mount='mount'+id[0].toUpperCase()+id.slice(1);
|
||
assert.match(source,new RegExp('function '+mount+'\\('));
|
||
}
|
||
assert.equal((source.match(/getState:/g)||[]).length,learningIds.length);
|
||
});
|
||
|
||
test('nieuwe widgets zijn tweetalig beschreven', async()=>{
|
||
const core=await readFile('public/js/core.js','utf8');
|
||
for(const id of learningIds){
|
||
assert.equal((core.match(new RegExp('wg_'+id+':','g'))||[]).length,2,id);
|
||
assert.equal((core.match(new RegExp('wg_'+id+'_d:','g'))||[]).length,2,id);
|
||
}
|
||
});
|
||
|
||
test('wereldoriëntatiewidgets laden, registreren en vertalen', async()=>{
|
||
const [html,board,source,core]=await Promise.all([readFile('public/index.html','utf8'),readFile('public/js/board.js','utf8'),readFile('public/js/widgets/world.js','utf8'),readFile('public/js/core.js','utf8')]);
|
||
assert.ok(html.indexOf('js/widgets/world.js')<html.indexOf('js/board.js'));
|
||
for(const [id,mount] of [['trees','mountTrees'],['birds','mountBirds'],['plants','mountPlants'],['topography','mountTopography']]){
|
||
assert.ok(worldIds.includes(id));
|
||
assert.match(board,new RegExp('id:"'+id+'"[^\\n]*cat:"world"[^\\n]*mount:'+mount));
|
||
assert.match(source,new RegExp('function '+mount+'\\('));
|
||
assert.equal((core.match(new RegExp('wg_'+id+':','g'))||[]).length,2,id);
|
||
assert.equal((core.match(new RegExp('wg_'+id+'_d:','g'))||[]).length,2,id);
|
||
}
|
||
assert.equal((core.match(/catWorld:/g)||[]).length,2);
|
||
for(const key of ['worldActivity','worldLearn','worldQuiz','worldExercise','worldExerciseRecognize','worldExercisePair','worldExerciseClue','worldExercisePhoto','worldExerciseLatin','worldTreeView','worldQuestionLeaf','worldQuestionBird','worldQuestionPlant','worldQuestionPair','worldQuestionClue','worldQuestionLatin','worldFindTreePhoto','worldFindBirdPhoto','worldFindPlantPhoto','worldPhotoTree','worldPhotoLeaf','worldPhotoBird','worldPhotoPlant','worldPhotoCredit','worldPhotoOption','topoProvinces','topoCapitals','topoCities','topoRegions','topoRivers','topoContinents','topoFindProvince','topoFindCapital','topoFindCity','topoFindRegion','topoFindRiver','topoFindContinent','topoMapSource','topoWorldMapSource'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
|
||
for(const exercise of ['recognize','pair','clue','photo','latin'])assert.ok(source.includes('"'+exercise+'"'),exercise);
|
||
for(const mode of ['regions','rivers','continents'])assert.ok(source.includes('["'+mode+'","topo'),mode);
|
||
assert.match(source,/getState/);
|
||
assert.ok(source.includes('name.textContent=worldName(item)'));
|
||
assert.ok(source.includes('hint.textContent=worldHint(item)'));
|
||
assert.doesNotMatch(source,/innerHTML\s*=\s*(?:worldName|worldHint)\s*\(/);
|
||
});
|
||
|
||
test('wereldoriëntatiedata bevat echte soortmedia en uitgebreide topografie', async()=>{
|
||
const [source,map,worldMap,attribution]=await Promise.all([
|
||
readFile('public/js/widgets/world.js','utf8'),
|
||
readFile('public/img/world/netherlands-provinces.svg','utf8'),
|
||
readFile('public/img/world/world-continents.svg','utf8'),
|
||
readFile('public/img/world/ATTRIBUTION.md','utf8'),
|
||
]);
|
||
const context={};
|
||
runInNewContext(source+';globalThis.__world={trees:WORLD_TREES,birds:WORLD_BIRDS,plants:WORLD_PLANTS,media:WORLD_MEDIA,provinces:NL_PROVINCES,cities:NL_CITIES,regions:NL_REGIONS,rivers:NL_RIVERS,continents:WORLD_CONTINENTS}',context);
|
||
const data=JSON.parse(JSON.stringify(context.__world));
|
||
assert.equal(data.trees.length,18);
|
||
assert.equal(data.birds.length,18);
|
||
assert.equal(data.plants.length,12);
|
||
assert.equal(data.provinces.length,12);
|
||
assert.equal(data.cities.filter(city=>city.capital).length,12);
|
||
assert.equal(data.cities.filter(city=>!city.capital).length,8);
|
||
assert.equal(data.regions.length,4);
|
||
assert.equal(data.rivers.length,5);
|
||
assert.equal(data.continents.length,7);
|
||
for(const collection of [data.trees,data.birds,data.plants,data.provinces,data.cities,data.regions,data.rivers,data.continents])assert.equal(new Set(collection.map(item=>item.id)).size,collection.length);
|
||
for(const item of [...data.trees,...data.birds,...data.plants])assert.ok(item.nl&&item.en&&item.latin&&item.hintNl&&item.hintEn&&item.tier>=1&&item.tier<=3,item.id);
|
||
const assetPaths=[];
|
||
for(const tree of data.trees){
|
||
assert.ok(data.media[tree.id]?.tree?.file&&data.media[tree.id]?.leaf?.file,tree.id);
|
||
assetPaths.push(data.media[tree.id].tree.file,data.media[tree.id].leaf.file);
|
||
}
|
||
for(const bird of data.birds){
|
||
assert.ok(data.media[bird.id]?.bird?.file,bird.id);
|
||
assetPaths.push(data.media[bird.id].bird.file);
|
||
}
|
||
for(const plant of data.plants){
|
||
assert.ok(data.media[plant.id]?.plant?.file,plant.id);
|
||
assetPaths.push(data.media[plant.id].plant.file);
|
||
}
|
||
assert.equal(new Set(assetPaths).size,66);
|
||
for(const relativePath of assetPaths){
|
||
const image=await readFile('public/img/world/'+relativePath);
|
||
assert.ok(image.length>10_000,relativePath+' is geen bruikbare foto');
|
||
assert.equal(image.subarray(0,4).toString(),'RIFF',relativePath);
|
||
assert.equal(image.subarray(8,12).toString(),'WEBP',relativePath);
|
||
assert.ok(attribution.includes('`'+relativePath+'`'),relativePath+' mist bronvermelding');
|
||
}
|
||
const cities=new Map(data.cities.map(city=>[city.id,city]));
|
||
const capitals=Object.fromEntries(data.provinces.map(province=>[province.nl,cities.get(province.capital)?.nl]));
|
||
assert.deepEqual(capitals,{
|
||
Friesland:'Leeuwarden',Groningen:'Groningen',Drenthe:'Assen',Overijssel:'Zwolle',Flevoland:'Lelystad','Noord-Holland':'Haarlem',Utrecht:'Utrecht',Gelderland:'Arnhem','Zuid-Holland':'Den Haag',Zeeland:'Middelburg','Noord-Brabant':"'s-Hertogenbosch",Limburg:'Maastricht',
|
||
});
|
||
for(const province of data.provinces){
|
||
assert.ok(province.shape&&Number.isFinite(province.x)&&Number.isFinite(province.y)&&cities.has(province.capital),province.id);
|
||
assert.match(map,new RegExp('<path id="'+province.shape+'"[^>]+d="[^"]+"'));
|
||
}
|
||
assert.equal((map.match(/<path id=/g)||[]).length,12);
|
||
const regionProvinces=data.regions.flatMap(region=>region.provinces);
|
||
assert.equal(regionProvinces.length,12);
|
||
assert.equal(new Set(regionProvinces).size,12);
|
||
assert.deepEqual([...regionProvinces].sort(),data.provinces.map(province=>province.id).sort());
|
||
for(const river of data.rivers)assert.ok(/^M [\d ]+/.test(river.path)&&river.color&&Number.isFinite(river.labelX)&&Number.isFinite(river.labelY),river.id);
|
||
for(const continent of data.continents){
|
||
assert.ok(continent.shape&&continent.color&&Number.isFinite(continent.x)&&Number.isFinite(continent.y),continent.id);
|
||
assert.match(worldMap,new RegExp('<g id="'+continent.shape+'">'));
|
||
}
|
||
assert.equal((worldMap.match(/<g id=/g)||[]).length,7);
|
||
assert.ok(Buffer.byteLength(worldMap)<200_000,'de wereldkaart moet compact genoeg blijven voor mobiel gebruik');
|
||
assert.ok(attribution.includes('Natural Earth 1:110m Geography Regions Polygons'));
|
||
assert.ok(attribution.includes('CBS-indeling voor Gebieden in Nederland 2026'));
|
||
assert.ok(attribution.includes('Rijkswaterstaat-overzicht Rivieren'));
|
||
assert.match(source,/href:TOPO_MAP_ASSET\+"#"\+province\.shape/);
|
||
assert.match(source,/href:TOPO_WORLD_ASSET\+"#"\+continent\.shape/);
|
||
});
|
||
|
||
test('widgetbibliotheek bevat zoeken favorieten sjablonen en borddata-acties', async()=>{
|
||
const [html,board]=await Promise.all([readFile('public/index.html','utf8'),readFile('public/js/board.js','utf8')]);
|
||
for(const id of ['gallerySearch','galleryFavOnly','galleryTemplates','boardUndo','boardRedo','boardExport','boardImport'])assert.match(html,new RegExp('id="'+id+'"'));
|
||
for(const feature of ['teachWidgetFavorites','structuredClone','application/json','widgetDuplicate','widgetLock','widgetPreview'])assert.ok(board.includes(feature),feature);
|
||
});
|
||
|
||
|
||
test('oefenklok biedt drie niveaus leerwijzen en meerdere spellen', async()=>{
|
||
const [source,core]=await Promise.all([readFile('public/js/widgets/learning.js','utf8'),readFile('public/js/core.js','utf8')]);
|
||
for(const value of ["'analogue'","'digital'","'words'","'both'","'set'","'read'","'choose'","amode:'blocks'","clock-type-answer","clock-task-digital","clock-task-words"])assert.ok(source.includes(value),value);
|
||
assert.match(source,/S\.level===1\?\[0,30\]:S\.level===2\?Array\.from\(\{length:12\}/);
|
||
for(const key of ['clockLevel','clockLearningStyle','clockGame','clockScore','clockLevel1Hint','clockLevel2Hint','clockLevel3Hint','clockTypeHint','clock24Hour'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
|
||
});
|
||
|
||
|
||
test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellingen', async()=>{
|
||
const [source,css,core]=await Promise.all([readFile('public/js/widgets/learning.js','utf8'),readFile('public/css/teach.css','utf8'),readFile('public/js/core.js','utf8')]);
|
||
for(const feature of ['onpointerdown','onpointermove','setPointerCapture','releasePointerCapture','getBoundingClientRect','clock-time-blocks','dotGrid'])assert.ok(source.includes(feature),feature);
|
||
assert.match(css,/clock\.interactive[^}]*touch-action:none/);
|
||
assert.match(source,/learningShell\(root,\[field\(T\('clockLearningStyle'\),style\),field\(T\('clockGame'\),game\),linkSetting,hour24Setting\],stage\)/);
|
||
const clockSource=source.slice(source.indexOf('function mountClock'),source.indexOf('function mountPlacevalue'));
|
||
assert.doesNotMatch(clockSource,/learningShell\(root,\[[^\]]*(?:numberlineCheck|numberlineNew|clockHour|clockMinute)/);
|
||
for(const feature of ['answerDigits','clock-answer-slots','clock-block-tray','dragClone','droptarget',"'0123456789'.split('')",'clock-keypad',"S.amode==='blocks'?'🧱':'⌨️'"])assert.ok(clockSource.includes(feature),feature);
|
||
assert.match(clockSource,/String\(hour\)\.padStart\(2,'0'\)\+String\(minute\)\.padStart\(2,'0'\)/);
|
||
assert.equal((core.match(/clockIncomplete:/g)||[]).length,2);
|
||
assert.ok(clockSource.includes("'clock-tick hour'"), 'uurposities krijgen een eigen korte markering');
|
||
assert.match(css,/clock-tick\.hour[^}]*width:4px[^}]*height:5%[^}]*background:var\(--blue\)/);
|
||
assert.match(css,/translateY\(calc\(var\(--clock-size\)\*-.34\)\)/);
|
||
assert.doesNotMatch(css,/clock-score\{margin-left:auto\{/);
|
||
assert.ok(clockSource.includes("linkHands:true"));
|
||
assert.ok(clockSource.includes("linkHands.type='checkbox'"));
|
||
assert.ok(clockSource.includes("linkSetting.hidden=game.value!=='set'"));
|
||
assert.ok(clockSource.includes('for(let hour=1;hour<=12;hour++)'), 'gekoppelde urenwijzer rekent de volledige tijd uit');
|
||
assert.ok(clockSource.includes("game.onchange=()=>{syncSettings();newTask()}"));
|
||
assert.doesNotMatch(clockSource,/clockStartTask|clockWelcomeHint/);
|
||
assert.equal((core.match(/clockLinkHands:/g)||[]).length,2);
|
||
assert.ok(clockSource.includes('Math.floor(Math.random()*24)'), 'niveau 3 gebruikt de volledige dag');
|
||
assert.ok(clockSource.includes('S.level===3&&S.use24'), '24-uursklok is een niveau-3-optie');
|
||
assert.ok(clockSource.includes("hour24Setting.hidden=S.level!==3"));
|
||
assert.ok(clockSource.includes("use24.onchange=()=>{S.use24=use24.checked;newTask()}"));
|
||
assert.ok(clockSource.includes("period?'12–23':'00–11'"), '24-uursmodus laat de juiste daghelft kiezen');
|
||
assert.ok(clockSource.includes('face._dragAngle=S.h*30+S.m/2'), 'de urenwijzer start vanaf de actuele positie binnen 24 uur');
|
||
assert.ok(clockSource.includes('for(let hour=0;hour<24;hour++)'), 'de urenwijzer kan alle 24 uren aanwijzen');
|
||
assert.ok(clockSource.includes('button.dataset.period=period'), 'de daghelft-indicator kan tijdens het draaien live wisselen');
|
||
assert.ok(clockSource.includes("const expectedHour=S.game==='set'?(uses24()?S.targetH:clockHour(S.targetH)):S.targetH"), '24-uursantwoorden worden exact gecontroleerd');
|
||
assert.ok(clockSource.includes('S.h=uses24()?0:12'), 'een 24-uursopdracht begint met een geldige invoerwaarde');
|
||
assert.ok(clockSource.includes("hourHand.onpointerdown=e=>start(e,'hour')"));
|
||
assert.ok(clockSource.includes("minuteHand.onpointerdown=e=>start(e,'minute')"));
|
||
assert.ok(clockSource.includes("if(hand==='minute')face._lastPointerAngle=null"), 'de minutenwijzer volgt zijn draairichting over twaalf uur');
|
||
assert.ok(clockSource.includes('if(step)S.h=clockStepHour(S.h,step,uses24())'), 'een volledige minutenomloop verzet de urenwijzer');
|
||
assert.ok(!clockSource.includes('face.onpointerdown='), 'de hele klok is geen sleepzone');
|
||
assert.doesNotMatch(css,/\.clock-hand::(?:before|after)/, 'geen extra sleepzones rond de wijzers');
|
||
for(const key of ['clockMorning','clockAfternoon','clockEvening','clockNight'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
|
||
assert.match(css,/clock-top\{[^}]*position:sticky[^}]*flex-wrap:wrap/);
|
||
assert.match(css,/clock-app\{[^}]*justify-content:flex-start/);
|
||
assert.match(css,/clock-set-answer\{[^}]*align-items:center[^}]*justify-content:center/);
|
||
assert.match(css,/clock-period \.tbtn\.active\{[^}]*background:var\(--purple\)/);
|
||
const context={};
|
||
runInNewContext(source+';globalThis.__clock24={delta:clockTurnDelta,crossing:clockHourCrossing,step:clockStepHour,angle:clock24Angle,hour:clock24Hour}',context);
|
||
assert.equal(context.__clock24.delta(2,358),4,'vooruit over twaalf uur blijft vooruit draaien');
|
||
assert.equal(context.__clock24.delta(358,2),-4,'achteruit over twaalf uur blijft achteruit draaien');
|
||
assert.equal(context.__clock24.crossing(2,358),1,'de grote wijzer verhoogt het uur bij een voorwaartse omloop');
|
||
assert.equal(context.__clock24.crossing(358,2),-1,'de grote wijzer verlaagt het uur bij een achterwaartse omloop');
|
||
assert.equal(context.__clock24.crossing(30,20),0,'bewegen binnen hetzelfde uur verzet de kleine wijzer niet');
|
||
assert.deepEqual([
|
||
context.__clock24.step(11,1,false),context.__clock24.step(12,1,false),context.__clock24.step(1,-1,false),
|
||
context.__clock24.step(23,1,true),context.__clock24.step(0,-1,true),
|
||
],[12,1,12,0,23]);
|
||
assert.equal(context.__clock24.angle(719,2),1,'na 23 uur draait de klok door naar 0 uur');
|
||
assert.equal(context.__clock24.angle(1,-2),719,'achteruit vanaf 0 uur komt de klok bij 23 uur');
|
||
assert.deepEqual([0,11,12,23].map(hour=>context.__clock24.hour(hour*30,0)),[0,11,12,23]);
|
||
});
|
||
|
||
test('geldrekenen biedt drie niveaus, leerwijzen, spellen en directe bediening', async()=>{
|
||
const [source,css,core,board]=await Promise.all([readFile('public/js/widgets/learning.js','utf8'),readFile('public/css/teach.css','utf8'),readFile('public/js/core.js','utf8'),readFile('public/js/board.js','utf8')]);
|
||
const moneySource=source.slice(source.indexOf('function mountMoney'),source.indexOf('function mountWordtypes'));
|
||
for(const value of ["['make','moneyGameMake']","['count','moneyGameCount']","['change','moneyGameChange']","['amount','moneyStyleAmount']","['words','moneyStyleWords']","['both','moneyStyleBoth']","S.amode==='blocks'?'🧱':'⌨️'"])assert.ok(moneySource.includes(value),value);
|
||
assert.ok(moneySource.includes("const levelRule=()=>MONEY_LEVELS[S.level]"));
|
||
assert.ok(source.includes("function createMoneyTask(level,game,category='all'"));
|
||
assert.ok(moneySource.includes("denominations=()=>levelRule().denominations"));
|
||
assert.ok(moneySource.includes("S.game==='change'"));
|
||
for(const feature of ['money-wallet','money-cash-tray','money-answer-slots','money-digit-tray','money-keypad','dragClone','droptarget','getBoundingClientRect','amountWords','money-piece-image','money-product-icon','money-product-name','money-level-hint'])assert.ok(moneySource.includes(feature),feature);
|
||
assert.ok(moneySource.includes("learningShell(root,[styleSetting,gameSetting,categorySetting],stage)"),'alleen voorkeuren staan onder instellingen');
|
||
assert.ok(moneySource.includes("style.onchange=draw"));
|
||
assert.ok(moneySource.includes("game.onchange=()=>{S.game=game.value;syncSettings();newTask()}"));
|
||
assert.ok(moneySource.includes("category.onchange=()=>{S.category=category.value;newTask()}"));
|
||
assert.ok(moneySource.includes("categorySetting.hidden=S.game!=='change'"));
|
||
assert.doesNotMatch(moneySource,/learningShell\(root,\[[^\]]*(?:numberlineCheck|numberlineNew|moneyClear|moneyNextTask)/);
|
||
for(const key of ['moneyLearningStyle','moneyStyleAmount','moneyStyleWords','moneyStyleBoth','moneyGame','moneyGameMake','moneyGameCount','moneyGameChange','moneyLevel','moneyLevel1Hint','moneyLevel2Hint','moneyLevel3Hint','moneyScore','moneyMakeTitle','moneyCountTitle','moneyChangeTitle','moneyWallet','moneyTray','moneyBuildHint','moneyTypeHint','moneyCorrect','moneyNextTask','moneyRemove','moneyCategory','moneyCategoryAll','moneyCategoryFood','moneyCategorySchool','moneyCategoryPlay'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
|
||
assert.match(css,/money-app\{[^}]*align-self:flex-start[^}]*justify-content:flex-start/);
|
||
assert.match(css,/money-wallet\.droptarget/);
|
||
assert.match(css,/money-wallet\.money-remove-target/);
|
||
assert.ok(moneySource.includes("pile(S.selected,true,wallet)"));
|
||
assert.ok(moneySource.includes("const moved=hasMoved(ev),outside=!over(ev)"));
|
||
assert.ok(moneySource.includes("moved&&outside"));
|
||
assert.match(css,/coin\[data-value="1"\]\{width:36px;height:36px/);
|
||
assert.match(css,/coin\[data-value="200"\]\{width:57px;height:57px/);
|
||
assert.match(css,/note\[data-value="500"\]\{width:82px;height:42px/);
|
||
assert.match(css,/note\[data-value="5000"\]\{width:93px;height:51px/);
|
||
assert.match(css,/money-piece\.coin\[data-value="100"\]/);
|
||
assert.match(css,/money-piece-image\{[^}]*object-fit:contain/);
|
||
assert.match(css,/@media\(max-width:520px\)[\s\S]*?money-shop-task\{grid-template-columns:minmax\(0,.8fr\) minmax\(0,1.2fr\)/);
|
||
assert.ok(moneySource.includes("image.src="));
|
||
assert.ok(moneySource.includes("/img/money/"));
|
||
assert.ok(moneySource.includes("image.draggable=false"));
|
||
assert.doesNotMatch(moneySource,/money-(?:arch|hologram|serial|stars|map|unit)/);
|
||
for(const file of ['coin-1.png','coin-2.png','coin-5.png','coin-10.png','coin-20.png','coin-50.png','coin-100.png','coin-200.png','note-500.jpg','note-1000.jpg','note-2000.jpg','note-5000.jpg']){
|
||
const asset=await readFile('public/img/money/'+file);
|
||
assert.ok(asset.length>5000,file);
|
||
}
|
||
assert.match(board,/id:"money"[^\n]*w:580, h:560, minW:380, minH:360/);
|
||
});
|
||
|
||
|
||
test('elk geldspel gebruikt geldige niveaugrenzen en realistische productcategorieën', async()=>{
|
||
const source=await readFile('public/js/widgets/learning.js','utf8'),context={};
|
||
runInNewContext(source+';globalThis.__moneyData={levels:MONEY_LEVELS,products:MONEY_PRODUCTS,createTask:createMoneyTask}',context);
|
||
const data=JSON.parse(JSON.stringify({levels:context.__moneyData.levels,products:context.__moneyData.products})),createTask=context.__moneyData.createTask,categories=['food','school','play'],games=['make','count','change'];
|
||
assert.equal(Object.keys(data.levels).length,3);
|
||
assert.ok(data.products.length>=24);
|
||
assert.equal(new Set(data.products.map(item=>item.id)).size,data.products.length);
|
||
for(const level of [1,2,3]){
|
||
const rule=data.levels[level];
|
||
assert.ok(rule.min<rule.max);
|
||
assert.ok(rule.payments.some(value=>value>rule.max),'niveau '+level+' heeft passend betaalgeld');
|
||
for(const category of categories){
|
||
const pool=data.products.filter(item=>item.category===category&&Array.isArray(item.prices[level])&&item.prices[level].length);
|
||
assert.ok(pool.length>=5,category+' heeft producten op niveau '+level);
|
||
}
|
||
for(const item of data.products){
|
||
for(const price of item.prices[level]||[]){
|
||
assert.ok(price>=rule.min&&price<=rule.max,item.id+' valt binnen niveau '+level);
|
||
assert.equal((price-rule.min)%rule.step,0,item.id+' volgt de stapgrootte van niveau '+level);
|
||
}
|
||
}
|
||
for(const game of games){
|
||
const taskCategories=game==='change'?categories:['all'];
|
||
for(const category of taskCategories){
|
||
for(let sample=0;sample<30;sample++){
|
||
const task=createTask(level,game,category,null,null);
|
||
assert.ok(task.targetCents>=rule.min&&task.targetCents<=rule.max,game+' niveau '+level);
|
||
assert.equal((task.targetCents-rule.min)%rule.step,0,game+' stapgrootte niveau '+level);
|
||
assert.ok(task.paidCents>task.targetCents,game+' betaalgeld niveau '+level);
|
||
if(game==='change'){
|
||
const product=data.products.find(item=>item.id===task.productId);
|
||
assert.equal(product.category,category);
|
||
assert.ok(product.prices[level].includes(task.targetCents));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for(const item of data.products){
|
||
assert.ok(item.icon&&item.nl&&item.en);
|
||
assert.ok(categories.includes(item.category));
|
||
}
|
||
});
|
||
|
||
test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-bord',async()=>{
|
||
const [board,pupil,css]=await Promise.all([
|
||
readFile('public/js/board.js','utf8'),
|
||
readFile('public/js/pupil.js','utf8'),
|
||
readFile('public/css/teach.css','utf8'),
|
||
]);
|
||
const registrySource=board.slice(board.indexOf('const REGISTRY'),board.indexOf('const WIDGET_CATS'));
|
||
const registered=[...registrySource.matchAll(/id:"([^"]+)"/g)].map(match=>match[1]).sort();
|
||
const coverage={
|
||
anchor:'.an-hint',
|
||
letters:'.lg-slots',
|
||
flits:'.fl-word',
|
||
hak:'.hk-word',
|
||
ball:'.bl-word',
|
||
sentence:'.sb-banks',
|
||
madd:'.mt-pad',
|
||
msub:'.mt-pad',
|
||
mmul:'.mt-pad',
|
||
mdiv:'.mt-pad',
|
||
trees:'.world-gallery',
|
||
birds:'.world-picture',
|
||
plants:'.world-picture',
|
||
topography:'.topo-map',
|
||
mpct:'.mt-pad',
|
||
numberline:'.nlw-settings',
|
||
fractions:'.fraction-wall',
|
||
clock:'.clock-task-card',
|
||
placevalue:'.place-grid',
|
||
money:'.money-task-card',
|
||
wordtypes:'.wordtypes',
|
||
write:'.wp-bar',
|
||
mind:'.mm-top',
|
||
notes:'.notes{',
|
||
timer:'.timer',
|
||
dice:'.dc-row',
|
||
names:'.np-result',
|
||
birthday:'.bd{',
|
||
schedule:'.schedule>div',
|
||
mood:'.mood',
|
||
poll:'.poll',
|
||
media:'.media-bar',
|
||
};
|
||
assert.deepEqual(registered,Object.keys(coverage).sort(),'ieder geregistreerd widgettype staat in de mobiele audit');
|
||
for(const [id,selector] of Object.entries(coverage)){
|
||
assert.ok(css.includes(selector),id+' mist responsive/breedte-onafhankelijke opmaak');
|
||
}
|
||
|
||
assert.match(css,/\.widget-body\{[^}]*container-type:inline-size[^}]*container-name:widget/);
|
||
assert.match(css,/@container widget \(max-width:520px\)/);
|
||
const compact=css.slice(css.indexOf('Widgetinhoud op telefoonformaat'));
|
||
for(const selector of [
|
||
'.lg,.mt,.dc,.np,.timer,.nlw,.sb',
|
||
'.wp-bar',
|
||
'.media-bar',
|
||
'.mm-top',
|
||
'.an-hint',
|
||
'.bd{grid-template-columns:1fr}',
|
||
'.sb-banks{grid-template-columns:1fr}',
|
||
'.edu-stage{align-items:safe center;justify-content:safe center',
|
||
'.money-task-card,.clock-task-card,.world-task-card',
|
||
'.world-gallery{grid-template-columns:1fr}',
|
||
'.world-photo-credit',
|
||
'.world-photo-answers{gap:6px}',
|
||
'.topo-map-wrap{padding:6px}',
|
||
'.world-map-wrap{padding:4px}',
|
||
'.topo-world-legend{gap:4px}',
|
||
]){
|
||
assert.ok(compact.includes(selector),selector+' mist in de compacte containerregels');
|
||
}
|
||
|
||
const sizeCode=board.slice(
|
||
board.indexOf('function widgetSizeBounds'),
|
||
board.indexOf('/* stabiel widget-instantie-id'),
|
||
);
|
||
const context={board:{clientWidth:320,clientHeight:480}};
|
||
runInNewContext(sizeCode+';globalThis.mobileSize={bounds:widgetSizeBounds({minW:640,minH:540}),size:clampWidgetSize(760,640,{minW:640,minH:540})}',context);
|
||
assert.deepEqual(JSON.parse(JSON.stringify(context.mobileSize)),{
|
||
bounds:{minW:312,minH:472,maxW:312,maxH:472},
|
||
size:{w:312,h:472},
|
||
});
|
||
assert.ok(board.includes('new ResizeObserver(fitWidgetsToBoard).observe(board)'));
|
||
assert.ok(board.includes('win.dataset.widget = def.id'));
|
||
assert.match(pupil,/card\.style\.width = .min\(100%, \$\{def\.w\}px\)./);
|
||
assert.ok(pupil.includes('card.style.minWidth = "0"'));
|
||
});
|