All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 1m8s
247 lines
14 KiB
JavaScript
247 lines
14 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,sanitize:avatarSanitize,variants:AVATAR_VARIANTS,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',
|
|
speciesVariant:'rex',
|
|
bodyColor:'#3fae6a',
|
|
hairStyle:'none',
|
|
hairColor:'#5b3a29',
|
|
accessoryHead:'none',
|
|
accessoryBody:'none',
|
|
};
|
|
|
|
test("iedere categorie heeft vier anatomisch herkenbare avatarvarianten",async()=>{
|
|
const api=await loadAvatarRenderer();
|
|
const variants={
|
|
dino:["rex","triceratops","stegosaurus","raptor"],
|
|
monster:["horned","cyclops","slime","alien"],
|
|
animal:["cat","dog","rabbit","bear"],
|
|
};
|
|
for(const [species,ids] of Object.entries(variants)){
|
|
assert.deepEqual(Array.from(api.variants[species]),ids);
|
|
const bodies=[];
|
|
for(const id of ids){
|
|
const markup=api.avatarBodyMarkup(species,base.bodyColor,id);
|
|
bodies.push(markup);
|
|
assert.ok(markup.includes("av-creature-"+id),species+"/"+id+" mist variantklasse");
|
|
assert.equal((markup.match(/av-arm av-arm-/g)||[]).length,2,species+"/"+id+" mist twee armen");
|
|
assert.equal((markup.match(/av-leg av-leg-/g)||[]).length,2,species+"/"+id+" mist twee benen");
|
|
for(const part of ["av-neck","av-torso-"+species,"av-head-large","av-eye"]){
|
|
assert.ok(markup.includes(part),species+"/"+id+" mist "+part);
|
|
}
|
|
assert.ok(markup.indexOf("av-arm")<markup.indexOf("av-torso"),id+"-armen moeten achter de romp aansluiten");
|
|
assert.ok(markup.indexOf("av-leg")<markup.indexOf("av-torso"),id+"-benen moeten onder de romp aansluiten");
|
|
assert.ok(markup.indexOf("av-torso")<markup.indexOf("av-head-large"),id+"-kop moet de romp overlappen");
|
|
const svg=api.renderAvatarFace({...base,species,speciesVariant:id},"lg");
|
|
assert.ok(svg.attributes.class.includes("av-variant-"+id),id+" ontbreekt op SVG-klasse");
|
|
}
|
|
assert.equal(new Set(bodies).size,4,species+"-varianten moeten een uniek lichaam hebben");
|
|
}
|
|
|
|
const markup=(species,id)=>api.avatarBodyMarkup(species,base.bodyColor,id);
|
|
assert.ok(markup("dino","rex").includes("av-tail-rex"));
|
|
assert.ok(markup("dino","triceratops").includes("av-frill-triceratops"));
|
|
assert.ok(markup("dino","triceratops").includes("av-nose-horn"));
|
|
assert.ok(markup("dino","stegosaurus").includes("av-dino-plates"));
|
|
assert.ok(markup("dino","raptor").includes("av-feather-crest-raptor"));
|
|
assert.ok(markup("dino","raptor").includes("av-feather-chest"));
|
|
|
|
assert.ok(markup("monster","horned").includes("av-horn-left"));
|
|
assert.equal((markup("monster","cyclops").match(/class="av-part av-eye/g)||[]).length,1);
|
|
assert.ok(markup("monster","cyclops").includes("av-cyclops-eye"));
|
|
assert.ok(markup("monster","slime").includes("av-slime-drop"));
|
|
assert.equal((markup("monster","alien").match(/av-alien-eye/g)||[]).length,3);
|
|
assert.ok(markup("monster","alien").includes("av-antenna-tip"));
|
|
|
|
assert.ok(markup("animal","cat").includes("av-whiskers"));
|
|
assert.ok(markup("animal","dog").includes("av-ear-dog"));
|
|
assert.ok(markup("animal","dog").includes("av-tongue"));
|
|
assert.ok(markup("animal","rabbit").includes("av-ear-rabbit"));
|
|
assert.ok(markup("animal","rabbit").includes("av-rabbit-teeth"));
|
|
assert.ok(markup("animal","bear").includes("av-ear-bear"));
|
|
assert.ok(markup("animal","bear").includes("av-muzzle-bear"));
|
|
|
|
assert.equal(api.sanitize({species:"dino"}).speciesVariant,"rex");
|
|
assert.equal(api.sanitize({species:"monster"}).speciesVariant,"horned");
|
|
assert.equal(api.sanitize({species:"animal"}).speciesVariant,"cat");
|
|
assert.equal(api.sanitize({species:"animal",speciesVariant:"rabbit"}).speciesVariant,"rabbit");
|
|
assert.equal(api.sanitize({species:"monster",speciesVariant:"rabbit"}).speciesVariant,"horned");
|
|
});
|
|
test("haar volgt de kop met natuurlijke details en correcte lagen",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 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"));
|
|
assert.ok(markup.indexOf("av-hair-front-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"));
|
|
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("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 cyclopsGlasses=api.renderAvatarFace({...base,species:"monster",speciesVariant:"cyclops",accessoryHead:"glasses"},"lg").innerHTML;
|
|
const cyclopsPatch=api.renderAvatarFace({...base,species:"monster",speciesVariant:"cyclops",accessoryHead:"eyepatch"},"lg").innerHTML;
|
|
const alienGlasses=api.renderAvatarFace({...base,species:"monster",speciesVariant:"alien",accessoryHead:"glasses"},"lg").innerHTML;
|
|
const alienShades=api.renderAvatarFace({...base,species:"monster",speciesVariant:"alien",accessoryHead:"sunglasses"},"lg").innerHTML;
|
|
assert.ok(cyclopsGlasses.includes("av-glasses-cyclops"),"cycloopbril hoort rond het ene oog");
|
|
assert.ok(cyclopsPatch.includes("av-eyepatch-cyclops"),"cycloopooglap hoort voor het ene oog");
|
|
assert.ok(alienGlasses.includes("av-glasses-alien"),"alienbril hoort rond drie ogen");
|
|
assert.ok(alienShades.includes("av-sunglasses-alien"),"alienzonnebril hoort alle drie ogen af te dekken");
|
|
|
|
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 12960 variant-, haar- en accessoirecombinaties renderen volledig",async()=>{
|
|
const api=await loadAvatarRenderer();
|
|
let combinations=0;
|
|
for(const speciesId of ["dino","monster","animal"]){
|
|
for(const variantId of api.variants[speciesId]){
|
|
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,
|
|
speciesVariant:variantId,
|
|
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"));
|
|
assert.ok(svg.innerHTML.includes("av-creature-"+variantId));
|
|
combinations++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.equal(combinations,12960);
|
|
});
|
|
test("client, API en beide talen bevatten dezelfde avataropties",async()=>{
|
|
const [apiSource,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"];
|
|
const variants=["rex","triceratops","stegosaurus","raptor","horned","cyclops","slime","alien","cat","dog","rabbit","bear"];
|
|
for(const id of hair){
|
|
assert.ok(apiSource.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(apiSource.includes("'"+id+"'"),id+" ontbreekt in API-validatie");
|
|
assert.equal(core.split("avatarAcc_"+id+":").length-1,2,id+" mist een vertaling");
|
|
}
|
|
for(const id of variants){
|
|
assert.ok(apiSource.includes("'"+id+"'"),id+" ontbreekt in API-variantvalidatie");
|
|
assert.equal(core.split("avatarVariant_"+id+":").length-1,2,id+" mist een variantvertaling");
|
|
}
|
|
assert.ok(apiSource.includes("speciesVariant"),"API slaat de soortvariant niet op");
|
|
assert.equal(core.split("avatarVariant:").length-1,2,"variantveld mist een label");
|
|
});
|