73 lines
8.5 KiB
JavaScript
73 lines
8.5 KiB
JavaScript
import {chromium} from '@playwright/test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {pathToFileURL} from 'node:url';
|
|
const root=path.resolve(import.meta.dirname,'..');
|
|
const records=JSON.parse(fs.readFileSync(path.join(root,'assets/animejs/catalogs/presentation-patterns.json')));
|
|
assert.equal(records.length,4);
|
|
const manifest=JSON.parse(fs.readFileSync(path.join(root,'dist/animejs-gallery-manifest.json')));
|
|
const evidence=path.join(root,'evidence');fs.mkdirSync(evidence,{recursive:true});
|
|
const report={status:'running',runs:[],screenshots:[],network:[],errors:[]};
|
|
const browser=await chromium.launch({headless:true,...(process.env.CHROMIUM_EXECUTABLE?{executablePath:process.env.CHROMIUM_EXECUTABLE}:{})});
|
|
try {
|
|
for(const variant of ['source','dist']) {
|
|
const context=await browser.newContext({offline:true,viewport:{width:1280,height:1000}}),page=await context.newPage();
|
|
page.on('pageerror',e=>report.errors.push(String(e)));page.on('console',m=>{if(m.type()==='error')report.errors.push(m.text());});
|
|
context.on('request',r=>{if(!/^(file|data):/.test(r.url()))report.network.push(r.url());});
|
|
for(const record of records){
|
|
const id=record.id;
|
|
await page.goto(pathToFileURL(path.join(root,variant==='source'?'assets/animejs':'dist/'+manifest.package,'examples',id+'.html')).href);
|
|
const call=(method,arg)=>page.evaluate(({id,method,arg})=>animeGallery.controllers[id][method](arg),{id,method,arg});
|
|
const svg=()=>page.locator('svg').evaluate(e=>e.outerHTML);
|
|
const attr=(selector,name)=>page.locator(selector).first().getAttribute(name);
|
|
const final=await svg();
|
|
for(const t of [0,730,1550,2200,2700,3050,3750,4500,5400,6000]){await call('seek',6000);await call('seek',t);const back=await svg();await call('seek',0);await call('seek',t);assert.equal(await svg(),back,id+' exact bidirectional seek '+t);}
|
|
// Live test observes geometry, not just a changing timeline counter.
|
|
async function visiblePlayback(){await call('replay');const first=await svg();await page.waitForTimeout(1700);assert.notEqual(await svg(),first,id+' visible playback frozen');}
|
|
await visiblePlayback();
|
|
await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
|
|
assert.equal(await svg(),final,id+' natural speed-1 completion');
|
|
// Negative control substitutes a finite clock with NO visible tween. Same detector must reject it.
|
|
await page.evaluate(id=>{window.savedRenderer=industryRenderers[id];industryRenderers[id]=(svg,c,{anime,el})=>{el('rect',{x:200,y:200,width:80,height:50},'',svg);return anime.createTimeline({autoplay:false}).add({clock:0},{clock:1,duration:6000});};animeGallery.controllers[id].destroy();animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),animeExamples.find(c=>c.id===id));},id);
|
|
await assert.rejects(visiblePlayback(),/visible playback frozen/);
|
|
await page.evaluate(id=>{animeGallery.controllers[id].destroy();industryRenderers[id]=savedRenderer;animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),animeExamples.find(c=>c.id===id));},id);
|
|
if(id.endsWith('expressive-robot')){
|
|
await call('seek',740);assert.equal(Number(await attr('[data-eye]','ry')),.65,'visor closed');
|
|
await call('seek',1000);assert.equal(Number(await attr('[data-eye]','ry')),3.6,'visor reopened');
|
|
await call('seek',1550);assert.equal(Number(await attr('[data-hand="1"]','cy')),-17);assert.equal(Number(await attr('[data-elbow="1"]','cx')),35);
|
|
const raised=await attr('[data-arm="1"]','d');await call('seek',1750);assert.notEqual(await attr('[data-arm="1"]','d'),raised,'articulated scratch movement');
|
|
await call('seek',3300);assert.equal(Number(await attr('[data-hand="1"]','cy')),14,'hand returned');
|
|
await call('seek',0);const neutral=await attr('[data-smile]','d');await call('settle');assert.notEqual(await attr('[data-smile]','d'),neutral,'smile');
|
|
}
|
|
if(id.endsWith('approval-gate')){
|
|
for(let t=0;t<=6000;t+=50){await call('seek',t);const geometry=await page.evaluate(()=>{const q=s=>document.querySelector(s),x=new DOMMatrix(getComputedStyle(q('[data-work-item]')).transform).m41;return {left:330+x,right:390+x,topEnd:Number(q('[data-leaf="top"]').getAttribute('y'))+52,bottomStart:Number(q('[data-leaf="bottom"]').getAttribute('y'))};});if(geometry.right>467&&geometry.left<493)assert.ok(geometry.topEnd<=250&&geometry.bottomStart>=330,'never intersect closed barrier');if(t<3000)assert.equal(geometry.left,330,'wait for complete opening');}
|
|
await call('seek',3990);assert.equal(Number(await attr('[data-evidence-packet]','opacity')),0);await call('settle');assert.equal(Number(await attr('[data-evidence-packet]','cx')),180);
|
|
}
|
|
if(id.endsWith('delivery-cycle')){
|
|
const edges=await page.locator('[data-edge]').evaluateAll(es=>es.map(e=>e.dataset.edge));assert.deepEqual(edges,['0-1','1-2','1-3','2-4','3-4','4-5','5-6','6-7','7-0']);
|
|
await call('seek',1650);for(const edge of ['1-2','1-3'])assert.equal(Number(await attr(`[data-edge="${edge}"]`,'stroke-dashoffset')),0);
|
|
assert.equal(Number(await attr('[data-edge="4-5"]','stroke-dashoffset')),1,'acceptance waits for join');
|
|
}
|
|
if(id.endsWith('connected-memory')){
|
|
const routes=await page.locator('[data-source-route]').evaluateAll(es=>es.map(e=>({from:e.dataset.from,to:e.dataset.to,d:e.getAttribute('d')})));
|
|
assert.equal(routes.length,5);for(let i=0;i<5;i++){assert.equal(routes[i].from,'source-'+i);assert.equal(routes[i].to,'agent');assert.ok(routes[i].d.endsWith('L109 177'));assert.ok(routes[i].d.startsWith(`M${[110,290,470,650,830][i]} 395`));}
|
|
assert.equal(await page.locator('[data-source]').count(),5);
|
|
await call('settle');const ends=await page.locator('[data-source-packet]').evaluateAll(es=>es.map(e=>{const m=new DOMMatrix(getComputedStyle(e).transform);return [m.m41,m.m42];}));for(const [x,y] of ends){assert.ok(Math.abs(x-109)<.1);assert.ok(Math.abs(y-177)<.1);}
|
|
}
|
|
if(variant==='dist')for(const [name,time] of [['final',6000],['mid',id.endsWith('expressive-robot')?1900:id.endsWith('approval-gate')?2450:3500]]){await call('seek',time);const file=`presentation-patterns-${id.replace('presentation-patterns-','')}-${name}.png`;await page.locator('article').screenshot({path:path.join(evidence,file)});report.screenshots.push(file);}
|
|
for(const duration of [3000,9000,18000]){
|
|
await page.evaluate(({id,duration})=>{animeGallery.controllers[id].destroy();const c=structuredClone(animeExamples.find(c=>c.id===id));Object.assign(c.config,{duration,scale:4,smileDepth:14,travelPx:450});animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),c);},{id,duration});
|
|
assert.equal((await call('state')).duration,duration);assert.equal(Number(await page.locator('[data-beat]').last().getAttribute('data-beat')),duration);
|
|
for(const t of [0,duration*.25,duration*.5,duration]){await call('seek',duration);await call('seek',t);const a=await svg();await call('seek',0);await call('seek',t);assert.equal(await svg(),a);const bad=await page.locator('svg').evaluate(svg=>{const box=svg.getBoundingClientRect();return [...svg.querySelectorAll('text,rect,circle,ellipse,path')].filter(e=>{if(getComputedStyle(e).opacity==='0')return false;const r=e.getBoundingClientRect();return r.width&&r.height&&(r.left<box.left-2||r.right>box.right+2||r.top<box.top-2||r.bottom>box.bottom+2);}).map(e=>e.outerHTML);});assert.deepEqual(bad,[],id+' edited bounds');}
|
|
}
|
|
await call('settle');const editedFinal=await svg();await page.emulateMedia({reducedMotion:'reduce'});await call('replay');await page.waitForTimeout(100);assert.equal(await svg(),editedFinal);assert.equal((await call('state')).paused,true);await call('seek',0);assert.equal(await svg(),editedFinal);await page.emulateMedia({reducedMotion:'no-preference'});
|
|
await call('destroy');await call('destroy');assert.equal(await page.locator('article svg').count(),0);
|
|
report.runs.push({variant,id,livePlayback:'speed 1 to natural completion',frozenNegativeControl:'rejected',semanticChecks:true,editedDurations:[3000,9000,18000],reducedMotion:true});
|
|
}
|
|
await context.close();
|
|
}
|
|
assert.deepEqual(report.errors,[]);assert.deepEqual(report.network,[]);report.status='passed';console.log(JSON.stringify({status:report.status,runs:report.runs.length,screenshots:report.screenshots.length}));
|
|
}catch(e){report.status='failed';report.error=String(e.stack);throw e;}
|
|
finally{fs.writeFileSync(path.join(evidence,'presentation-patterns-report.json'),JSON.stringify(report,null,2)+'\n');await browser.close();}
|