teach/test/avatar.test.js
Ramon 7237cb6290
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 35s
feat: breid compacte avatars uit (v0.4.47-beta)
2026-07-22 19:59:15 +02:00

168 lines
8.3 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 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')<markup.indexOf('av-torso'),species+'-armen moeten achter de compacte romp aansluiten');
assert.ok(markup.indexOf('av-leg')<markup.indexOf('av-torso'),species+'-benen moeten onder de compacte romp aansluiten');
assert.ok(markup.indexOf('av-torso')<markup.indexOf('av-head-large'),species+'-kop moet de bovenkant van het lijf overlappen');
}
assert.ok(bodies.dino.indexOf('av-tail')<bodies.dino.indexOf('av-head-large'));
assert.ok(bodies.dino.indexOf('av-crest')<bodies.dino.indexOf('av-head-large'));
assert.equal((bodies.monster.match(/av-horn/g)||[]).length,2);
assert.equal((bodies.animal.match(/av-ear av-ear-/g)||[]).length,2);
assert.ok(bodies.animal.includes('av-chest-fluff'));
assert.notEqual(bodies.dino,bodies.monster);
assert.notEqual(bodies.monster,bodies.animal);
});
test('haar heeft natuurlijke voor- en achterlagen en vier nieuwe stijlen',async()=>{
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 ['tuft','wavy','bob','braids']){
const markup=api.renderAvatarFace({...base,hairStyle:id,hairColor:'#123456'},'lg').innerHTML;
assert.ok(markup.includes('av-hair-'+id),id+' wordt niet getekend');
assert.ok(markup.includes('#123456'),id+' gebruikt de gekozen haarkleur niet');
}
const bob=api.renderAvatarFace({...base,hairStyle:'bob'},'lg').innerHTML;
assert.ok(bob.indexOf('av-hair-bob')<bob.indexOf('av-head-large'));
assert.ok(bob.indexOf('av-hair-bob-fringe')>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'));
assert.ok(braids.indexOf('av-hair-braids-front')>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('#123456'),'pet hoort de voorste haarlaag af te dekken');
assert.ok(!beanie.includes('#123456'),'muts hoort de voorste haarlaag af te dekken');
assert.ok(crown.includes('#123456'),'kroon hoort op het haar te rusten');
assert.ok(cappedBraids.includes('#123456'),'vlechten horen naast een pet zichtbaar te blijven');
});
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-torso'));
assert.ok(cape.indexOf('av-cape-clasp')>cape.indexOf('av-head-large'));
const backpack=api.renderAvatarFace({...base,accessoryBody:'backpack'},'lg').innerHTML;
assert.ok(backpack.indexOf('av-backpack"')<backpack.indexOf('av-torso'));
assert.ok(backpack.indexOf('av-backpack-strap')>backpack.indexOf('av-head-large'));
const satchel=api.renderAvatarFace({...base,accessoryBody:'satchel'},'lg').innerHTML;
assert.ok(satchel.indexOf('av-satchel"')<satchel.indexOf('av-torso'));
assert.ok(satchel.indexOf('av-satchel-strap')>satchel.indexOf('av-head-large'));
const wings=api.renderAvatarFace({...base,accessoryBody:'wings'},'lg').innerHTML;
assert.ok(wings.indexOf('av-wings')<wings.indexOf('av-torso'));
assert.ok(wings.indexOf('av-wing-clasp')>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');
}
});