teach/test/widgets.test.js
Ramon 100dc62087
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
feat: verbeter wereldorientatie met echte beelden (v0.3.70-beta)
2026-07-16 10:31:50 +02:00

360 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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','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'],['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','worldTreeView','worldQuestionLeaf','worldQuestionBird','worldPhotoTree','worldPhotoLeaf','worldPhotoBird','worldPhotoCredit','topoProvinces','topoCapitals','topoCities','topoFindProvince','topoFindCapital','topoFindCity','topoMapSource'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
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 volledige Nederlandse topografie', async()=>{
const [source,map,attribution]=await Promise.all([
readFile('public/js/widgets/world.js','utf8'),
readFile('public/img/world/netherlands-provinces.svg','utf8'),
readFile('public/img/world/ATTRIBUTION.md','utf8'),
]);
const context={};
runInNewContext(source+';globalThis.__world={trees:WORLD_TREES,birds:WORLD_BIRDS,media:WORLD_MEDIA,provinces:NL_PROVINCES,cities:NL_CITIES}',context);
const data=JSON.parse(JSON.stringify(context.__world));
assert.equal(data.trees.length,8);
assert.equal(data.birds.length,8);
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);
for(const collection of [data.trees,data.birds,data.provinces,data.cities])assert.equal(new Set(collection.map(item=>item.id)).size,collection.length);
for(const item of [...data.trees,...data.birds])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);
}
assert.equal(new Set(assetPaths).size,24);
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);
assert.match(source,/href:TOPO_MAP_ASSET\+"#"\+province\.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?'1223':'0011'"), '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',
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',
'.topo-map-wrap{padding:6px}',
]){
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"'));
});