61 lines
6.5 KiB
JavaScript
61 lines
6.5 KiB
JavaScript
import {chromium} from '@playwright/test';
|
|
import fs from 'node:fs';import path from 'node:path';import {pathToFileURL} from 'node:url';import assert from 'node:assert/strict';import crypto from 'node:crypto';
|
|
const root=path.resolve(import.meta.dirname,'..'),file=path.join(root,'dist/animejs-gallery-0.4.0.html');
|
|
const report={sha256:crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'),runs:[]};
|
|
assert.equal(report.sha256,JSON.parse(fs.readFileSync(path.join(root,'dist/animejs-gallery-manifest.json'))).sha256);
|
|
const browser=await chromium.launch({headless:true,...(process.env.CHROMIUM_EXECUTABLE?{executablePath:process.env.CHROMIUM_EXECUTABLE}:{})});
|
|
fs.mkdirSync(path.join(root,'evidence'),{recursive:true});
|
|
try {for(const reducedMotion of ['no-preference','reduce']) {
|
|
const ctx=await browser.newContext({offline:true,reducedMotion,viewport:{width:1200,height:1000}}),page=await ctx.newPage(),errors=[],network=[];
|
|
page.on('pageerror',e=>errors.push(String(e)));page.on('console',m=>{if(m.type()==='error')errors.push(m.text());});page.on('request',r=>{if(!r.url().startsWith('file:')&&!r.url().startsWith('data:'))network.push(r.url());});
|
|
await page.goto(pathToFileURL(file).href);
|
|
const ids=await page.evaluate(()=>Object.keys(animeGallery.controllers));assert.equal(ids.length,5);
|
|
assert.equal(new Set(ids).size,ids.length,'unique recipe IDs');
|
|
const configs=await page.evaluate(()=>animeExamples.map(({id,kind})=>({id,kind})));
|
|
for(const [group,kind] of [['elements','element'],['compositions','composition']]){
|
|
const registry=path.join(root,`references/registry-animejs-${group}.md`),text=fs.readFileSync(registry,'utf8');
|
|
const listed=[...text.matchAll(/^\| `([^`]+)` \|/gm)].map(m=>m[1]);
|
|
assert.deepEqual(listed.sort(),configs.filter(c=>c.kind===kind).map(c=>c.id).sort(),'registry matches executable '+group);
|
|
for(const match of text.matchAll(/\]\(([^)]+)\)/g)){
|
|
const target=match[1].split('#')[0];if(target&&!target.includes('://'))assert.ok(fs.existsSync(path.resolve(path.dirname(registry),target)),'missing registry target '+target);
|
|
}
|
|
}
|
|
for(const id of ids){
|
|
const call=(method,arg)=>page.evaluate(({id,method,arg})=>animeGallery.controllers[id][method](arg),{id,method,arg});
|
|
const state=()=>call('state');
|
|
const visual=()=>page.locator('#'+id+' svg').evaluate(e=>e.outerHTML);
|
|
const final=await visual(),s=await state();assert.ok(Number.isFinite(s.duration)&&s.duration>0);assert.equal(s.time,s.duration);
|
|
if(reducedMotion==='no-preference'){
|
|
if(id==='assemble-explode'){
|
|
await call('settle');await call('seek',s.duration*.45);const hold=await visual();
|
|
const offsets=await page.locator('#assemble-explode [data-part]').evaluateAll(parts=>parts.map(e=>new DOMMatrix(getComputedStyle(e).transform).m41));
|
|
assert.deepEqual(offsets,[-105,0,105],'backward seek into hold must remain exploded');
|
|
await call('seek',0);await call('seek',s.duration*.45);assert.equal(await visual(),hold,'hold state independent of seek direction');
|
|
}
|
|
await call('seek',s.duration*.3);const earlier=await visual();await call('seek',s.duration*.8);const later=await visual();assert.notEqual(earlier,later);
|
|
await call('seek',s.duration*.3);assert.equal(await visual(),earlier);await call('seek',s.duration*.8);assert.equal(await visual(),later);
|
|
await call('seek',-100);assert.equal((await state()).time,0);await call('seek',s.duration+100);assert.equal((await state()).time,s.duration);
|
|
await page.locator('#'+id+' input').focus();await page.keyboard.press('Home');assert.equal((await state()).time,0);
|
|
await page.keyboard.press('ArrowRight');assert.ok((await state()).time>0);
|
|
}else {await call('seek',0);assert.equal(await visual(),final);}
|
|
for(const action of ['play','replay']){
|
|
await page.locator(`#${id} [data-action="${action}"]`).focus();await page.keyboard.press(action==='play'?'Enter':'Space');
|
|
if(reducedMotion==='no-preference'){await page.waitForTimeout(90);const running=await state();assert.ok(running.time>0&&running.time<running.duration);assert.notEqual(await visual(),final);}
|
|
await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
|
|
assert.equal(await visual(),final);
|
|
}
|
|
await call('replay');await page.locator(`#${id} [data-action="settle"]`).click();assert.equal(await visual(),final);assert.equal((await state()).paused,true);
|
|
const readable=await page.locator('#'+id+' svg').evaluate(svg=>[...svg.querySelectorAll('text,rect,circle')].every(e=>{const b=e.getBoundingClientRect(),r=svg.getBoundingClientRect();return Number(getComputedStyle(e).opacity)>0&&b.width>0&&b.left>=r.left-1&&b.right<=r.right+1&&b.top>=r.top-1&&b.bottom<=r.bottom+1;}));assert.ok(readable,id+' visible and within SVG');
|
|
}
|
|
await page.screenshot({path:path.join(root,`evidence/animejs-gallery-${reducedMotion}.png`),fullPage:true});
|
|
await page.evaluate(()=>Object.values(animeGallery.controllers).forEach(c=>c.replay()));await page.emulateMedia({reducedMotion:'reduce'});
|
|
await page.waitForFunction(()=>Object.values(animeGallery.controllers).every(c=>{const s=c.state();return s.paused&&s.time===s.duration;}));
|
|
assert.ok(await page.evaluate(()=>Object.values(animeGallery.controllers).every(c=>{const s=c.state();return s.paused&&s.time===s.duration;})));
|
|
await page.emulateMedia({reducedMotion:'no-preference'});await page.evaluate(()=>{Object.values(animeGallery.controllers).forEach(c=>c.replay());dispatchEvent(new Event('beforeprint'));});
|
|
assert.ok(await page.evaluate(()=>Object.values(animeGallery.controllers).every(c=>{const s=c.state();return s.paused&&s.time===s.duration;})));
|
|
await page.evaluate(()=>{for(const [id,c] of Object.entries(animeGallery.controllers)){c.replay();c.destroy();c.destroy();if(!c.state().destroyed||document.getElementById(id).children.length)throw Error('cleanup failed');}for(const config of animeExamples)animeGallery.controllers[config.id]=animeGallery.mount(document.getElementById(config.id),config);});
|
|
assert.equal(await page.locator('article button').count(),15);
|
|
assert.deepEqual(errors,[]);assert.deepEqual(network,[]);report.runs.push({reducedMotion,examples:ids,deterministicSeek:reducedMotion==='no-preference',reducedMotionSeekSuppressed:reducedMotion==='reduce',keyboardControls:true,replay:true,finalVisibility:true,liveReducedMotion:true,printSettle:true,cleanupAndRemount:true,errors,network});await ctx.close();
|
|
}fs.writeFileSync(path.join(root,'evidence/animejs-gallery-test-report.json'),JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report,null,2));
|
|
}finally{await browser.close();}
|