import test from 'node:test'; import assert from 'node:assert/strict'; import {readFile} from 'node:fs/promises'; import {runInNewContext} from 'node:vm'; async function loadAvatarRenderer(){ const source=await readFile('public/js/avatar.js','utf8'); const cutoff=source.indexOf('/* bouwt de interactieve kiezer'); assert.ok(cutoff>0,'rendergedeelte van avatar.js ontbreekt'); const context={ T:key=>key, document:{ createElementNS(){ return { attributes:{}, innerHTML:'', setAttribute(name,value){this.attributes[name]=String(value);}, }; }, }, }; runInNewContext(source.slice(0,cutoff)+ ';globalThis.__avatar={renderAvatarFace,avatarBodyMarkup,hair:AVATAR_HAIR_MARKUP,bodyAccessories:AVATAR_BODY_ACC_MARKUP,headAccessories:AVATAR_HEAD_ACC_MARKUP,hairIds:AVATAR_HAIR,headIds:AVATAR_HEAD_ACC,bodyIds:AVATAR_BODY_ACC};', context); return context.__avatar; } const base={ species:'dino', bodyColor:'#3fae6a', hairStyle:'none', hairColor:'#5b3a29', accessoryHead:'none', accessoryBody:'none', }; test('avatars combineren een grote kop met een compact soortspecifiek lijf',async()=>{ const api=await loadAvatarRenderer(); const bodies={}; for(const species of ['dino','monster','animal']){ const markup=api.avatarBodyMarkup(species,base.bodyColor); bodies[species]=markup; assert.equal((markup.match(/av-arm av-arm-/g)||[]).length,2,species+' mist twee armen'); assert.equal((markup.match(/av-leg av-leg-/g)||[]).length,2,species+' mist twee benen'); for(const part of ['av-neck','av-torso-'+species,'av-head-large','av-eye']){ assert.ok(markup.includes(part),species+' mist '+part); } assert.ok(markup.indexOf('av-arm'){ const api=await loadAvatarRenderer(); assert.deepEqual(Array.from(api.hairIds),["none","tuft","spiky","curly","wavy","bob","ponytail","braids","mohawk"]); for(const id of api.hairIds){ assert.equal(typeof api.hair[id].back,"function",id+" mist haar-achterlaag"); assert.equal(typeof api.hair[id].front,"function",id+" mist haar-voorlaag"); } for(const id of api.hairIds.filter(id=>id!=="none")){ const front=api.hair[id].front("#123456"); const back=api.hair[id].back("#123456"); const combined=back+front; assert.ok(front.includes("av-hair-"+id),id+" mist een herkenbare voorlaag"); assert.ok(combined.includes("av-hair-texture"),id+" mist natuurlijke haartextuur"); assert.doesNotMatch(combined,/undefined|NaN/,id+" bevat ongeldige SVG-waarden"); } for(const id of ["curly","wavy","bob","ponytail","braids"]){ assert.notEqual(api.hair[id].back("#123456"),"",id+" mist volume achter de kop"); } for(const id of ["tuft","spiky","mohawk"]){ assert.equal(api.hair[id].back("#123456"),"",id+" hoort geen onzichtbare achterlaag te hebben"); } for(const id of ["wavy","bob","ponytail","braids"]){ assert.ok(api.hair[id].front("#123456").includes("av-hair-part"),id+" mist een natuurlijke scheiding"); } assert.ok(api.hair.wavy.front("#123456").includes("av-side-lock")); assert.ok(api.hair.bob.front("#123456").includes("av-side-lock")); assert.ok(api.hair.ponytail.back("#123456").includes("av-hair-tie")); assert.ok(api.hair.braids.back("#123456").includes("av-hair-tie")); const transforms={dino:"translate(1 0)",monster:"translate(0 -1)",animal:"translate(0 0)"}; for(const [species,transform] of Object.entries(transforms)){ const markup=api.renderAvatarFace({...base,species,hairStyle:"wavy",hairColor:"#123456"},"lg").innerHTML; assert.equal((markup.match(new RegExp("av-hair-fit-"+species,"g"))||[]).length,2,species+" mist voor- of achterpasvorm"); assert.ok(markup.includes(`transform="${transform}"`),species+" gebruikt een verkeerde kruincorrectie"); assert.ok(markup.indexOf("av-hair-back-layer")markup.indexOf("av-head-large")); } const bob=api.renderAvatarFace({...base,hairStyle:"bob"},"lg").innerHTML; assert.ok(bob.indexOf("av-hair-bob")bob.indexOf("av-head-large")); const braids=api.renderAvatarFace({...base,hairStyle:"braids"},"lg").innerHTML; assert.ok(braids.indexOf("av-hair-braids")braids.indexOf("av-head-large")); const cap=api.renderAvatarFace({...base,hairStyle:"spiky",hairColor:"#123456",accessoryHead:"cap"},"lg").innerHTML; const beanie=api.renderAvatarFace({...base,hairStyle:"spiky",hairColor:"#123456",accessoryHead:"beanie"},"lg").innerHTML; const crown=api.renderAvatarFace({...base,hairStyle:"spiky",hairColor:"#123456",accessoryHead:"crown"},"lg").innerHTML; const cappedBraids=api.renderAvatarFace({...base,hairStyle:"braids",hairColor:"#123456",accessoryHead:"cap"},"lg").innerHTML; assert.ok(!cap.includes("av-hair-front-layer"),"pet hoort kort haar af te dekken"); assert.ok(!beanie.includes("av-hair-front-layer"),"muts hoort kort haar af te dekken"); assert.ok(crown.includes("av-hair-front-layer"),"kroon hoort op het haar te rusten"); assert.ok(cappedBraids.includes("av-hair-back-layer"),"vlechten horen naast een pet zichtbaar te blijven"); assert.ok(!cappedBraids.includes("av-hair-front-layer"),"pet hoort de bovenkant van vlechten af te dekken"); }); test('accessoires gebruiken vaste ankers en correcte voor- en achterlagen',async()=>{ const api=await loadAvatarRenderer(); assert.deepEqual(Array.from(api.headIds),['none','cap','beanie','crown','wizardhat','glasses','sunglasses','eyepatch','bow','flower','headphones','partyhat']); assert.deepEqual(Array.from(api.bodyIds),['none','scarf','cape','badge','necklace','bowtie','belt','backpack','satchel','wings']); for(const id of api.bodyIds){ assert.equal(typeof api.bodyAccessories[id].back,'function',id+' mist achterlaag'); assert.equal(typeof api.bodyAccessories[id].front,'function',id+' mist voorlaag'); } for(const [id,part] of Object.entries({beanie:'av-beanie',wizardhat:'av-wizardhat',eyepatch:'av-eyepatch',flower:'av-flower'})){ const markup=api.renderAvatarFace({...base,accessoryHead:id},'lg').innerHTML; assert.ok(markup.indexOf(part)>markup.indexOf('av-head-large'),id+' hoort op de kop'); } const cape=api.renderAvatarFace({...base,accessoryBody:'cape'},'lg').innerHTML; assert.ok(cape.indexOf('av-cape"')cape.indexOf('av-head-large')); const backpack=api.renderAvatarFace({...base,accessoryBody:'backpack'},'lg').innerHTML; assert.ok(backpack.indexOf('av-backpack"')backpack.indexOf('av-head-large')); const satchel=api.renderAvatarFace({...base,accessoryBody:'satchel'},'lg').innerHTML; assert.ok(satchel.indexOf('av-satchel"')satchel.indexOf('av-head-large')); const wings=api.renderAvatarFace({...base,accessoryBody:'wings'},'lg').innerHTML; assert.ok(wings.indexOf('av-wings')wings.indexOf('av-head-large')); for(const [id,part] of [['scarf','av-scarf'],['badge','av-badge'],['necklace','av-necklace'],['bowtie','av-bowtie'],['belt','av-belt']]){ const markup=api.renderAvatarFace({...base,accessoryBody:id},'lg').innerHTML; assert.ok(markup.indexOf(part)>markup.indexOf('av-head-large'),id+' hoort voor op het lijf'); } }); test('alle 3240 avatarcombinaties renderen volledig binnen de SVG',async()=>{ const api=await loadAvatarRenderer(); let combinations=0; for(const speciesId of ['dino','monster','animal']){ for(const hairId of api.hairIds){ for(const headId of api.headIds){ for(const bodyId of api.bodyIds){ const svg=api.renderAvatarFace({ ...base, species:speciesId, hairStyle:hairId, accessoryHead:headId, accessoryBody:bodyId, },'sm'); assert.equal(svg.attributes.viewBox,'-8 -21 116 132'); assert.equal(svg.attributes.preserveAspectRatio,'xMidYMid meet'); assert.doesNotMatch(svg.innerHTML,/undefined|NaN/); assert.ok(svg.innerHTML.includes('av-head-large')); combinations++; } } } } assert.equal(combinations,3240); }); test('client, API en beide talen bevatten dezelfde nieuwe avataropties',async()=>{ const [api,core]=await Promise.all([ readFile('src/api.js','utf8'), readFile('public/js/core.js','utf8'), ]); const hair=['tuft','wavy','bob','braids']; const accessories=['beanie','wizardhat','eyepatch','flower','bowtie','belt','satchel','wings']; for(const id of hair){ assert.ok(api.includes("'"+id+"'"),id+' ontbreekt in API-validatie'); assert.equal(core.split('avatarHair_'+id+':').length-1,2,id+' mist een vertaling'); } for(const id of accessories){ assert.ok(api.includes("'"+id+"'"),id+' ontbreekt in API-validatie'); assert.equal(core.split('avatarAcc_'+id+':').length-1,2,id+' mist een vertaling'); } });