Files
presentation-html/scripts/test-gallery.mjs
T

71 lines
8.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,'..'),manifest=JSON.parse(fs.readFileSync(path.join(root,'dist/animejs-gallery-manifest.json')));
for(const [file,hash] of Object.entries(manifest.files))assert.equal(crypto.createHash('sha256').update(fs.readFileSync(path.join(root,'dist',manifest.package,file))).digest('hex'),hash);
const report={files:manifest.files,runs:[]},ids=['svg-draw','motion-path','spatial-stagger','dependency-flow','assemble-explode'];
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 variant of ['source','dist'])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());});
const base=path.join(root,variant==='source'?'assets/animejs':'dist/'+manifest.package),url=p=>pathToFileURL(path.join(base,p)).href;
const shot=async name=>{const size=page.viewportSize();await page.setViewportSize({width:size.width,height:await page.evaluate(()=>document.documentElement.scrollHeight)});await page.evaluate(()=>scrollTo(0,0));await page.waitForTimeout(100);await page.screenshot({path:path.join(root,`evidence/animejs-${variant}-${name}.png`),fullPage:true});await page.setViewportSize(size);};
for(const group of ['all','elements','compositions']){
await page.goto(url(group==='all'?'index.html':group+'/index.html'));
const expected=group==='all'?ids:group==='elements'?ids.slice(0,3):ids.slice(3);
assert.deepEqual(await page.locator('iframe').evaluateAll(f=>f.map(e=>e.dataset.example)),expected);
assert.equal(await page.evaluate(()=>typeof window.animeGallery),'undefined');
for(const id of expected){
const f=page.locator(`iframe[data-example="${id}"]`),target=url('examples/'+id+'.html');
assert.equal(await f.evaluate(e=>e.src),target);
assert.equal(await page.locator(`a.open-example[href$="/${id}.html"]`).evaluate(e=>e.href),target);
const inner=await(await f.elementHandle()).contentFrame();await inner.waitForFunction(()=>window.animeGallery);
assert.deepEqual(await inner.evaluate(()=>Object.keys(animeGallery.controllers)),[id]);assert.ok(await f.isVisible());assert.ok(await inner.locator('article h2').isVisible());assert.ok(await inner.locator('svg').isVisible());
}
if(group==='all')for(const g of ['elements','compositions'])assert.equal(await page.locator(`section[data-registry="${g}"] h2 a`).evaluate(e=>e.href),url(g+'/index.html'));
if(reducedMotion==='no-preference')await shot(group);
await page.locator('a.open-example').first().click();assert.equal(await page.locator('body').getAttribute('data-example'),expected[0]);
await page.getByRole('link',{name:'All examples',exact:true}).click();assert.equal(page.url(),url('index.html'));
}
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');
assert.deepEqual([...text.matchAll(/^\| `([^`]+)` \|/gm)].map(m=>m[1]).sort(),configs.filter(c=>c.kind===kind).map(c=>c.id).sort());
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)),target);}
}
for(const id of ids){
await page.goto(url('examples/'+id+'.html'));
assert.deepEqual(await page.evaluate(()=>Object.keys(animeGallery.controllers)),[id]);
assert.equal(await page.locator('h1').textContent(),await page.evaluate(id=>animeExamples.find(c=>c.id===id).title,id));
assert.ok(!(await page.content()).includes('ANIME_LICENSE'));
const group=ids.indexOf(id)<3?'elements':'compositions';await page.getByRole('link',{name:group[0].toUpperCase()+group.slice(1),exact:true}).click();assert.equal(page.url(),url(group+'/index.html'));await page.locator(`a.open-example[href$="/${id}.html"]`).click();
const call=(method,arg)=>page.evaluate(({id,method,arg})=>animeGallery.controllers[id][method](arg),{id,method,arg}),state=()=>call('state'),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('seek',s.duration*.45);const hold=await visual();assert.deepEqual(await page.locator('[data-part]').evaluateAll(parts=>parts.map(e=>new DOMMatrix(getComputedStyle(e).transform).m41)),[-105,0,105]);
await shot(id+'-hold');await call('seek',0);await call('seek',s.duration*.45);assert.equal(await visual(),hold);
}
await call('seek',s.duration*.3);const earlier=await visual();await shot(id+'-motion');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('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(`[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('[data-action="settle"]').click();assert.equal(await visual(),final);assert.equal((await state()).paused,true);
assert.ok(await page.locator('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;})),id+' bounds');
await shot(id+'-'+reducedMotion);
await call('replay');await page.emulateMedia({reducedMotion:'reduce'});await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
await page.emulateMedia({reducedMotion:'no-preference'});
for(const event of ['beforeprint','pagehide']){await call('replay');await page.evaluate(event=>dispatchEvent(new Event(event)),event);assert.equal((await state()).paused,true);assert.equal((await state()).time,s.duration);}
await call('replay');await call('destroy');await call('destroy');assert.ok((await state()).destroyed);assert.equal(await page.locator('#'+id+' > *').count(),0);
await page.evaluate(id=>{animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),animeExamples.find(c=>c.id===id));},id);assert.equal(await page.locator('article button').count(),3);assert.equal(await visual(),final);
await page.emulateMedia({reducedMotion});
}
assert.deepEqual(errors,[]);assert.deepEqual(network,[]);report.runs.push({variant,reducedMotion,examples:ids,hierarchy:true,exactIframeReuse:true,navigation:true,deterministicSeek:reducedMotion==='no-preference',reducedMotionSeekSuppressed:reducedMotion==='reduce',keyboardControls:true,replay:true,finalVisibility:true,liveReducedMotion:true,printAndDepartureSettle: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();}