[verified] feat: expand public animation library across 12 industries

This commit is contained in:
jarvis-at-skic committed 2026-10-25 18:14:35 +00:00
1 parent 3bb9555e6a
commit ecbbcf8ba0
144 files changed
+61323 -187

No files matched your search

+154 -46
View File
@@ -1,70 +1,178 @@
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')));
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';import vm from 'node:vm';
const root=path.resolve(import.meta.dirname,'..'),read=p=>fs.readFileSync(path.join(root,p),'utf8');
const manifest=JSON.parse(read('dist/animejs-gallery-manifest.json')),partial=process.argv.includes('--allow-partial');
assert.ok(manifest.complete||partial,'Incomplete release catalog: '+manifest.missingGroups.join(', ')+'. Use --allow-partial only for development.');
const sandbox={window:{}};vm.runInNewContext(read('assets/animejs/catalog-data.js'),sandbox);
const catalog=JSON.parse(JSON.stringify(sandbox.window.animeExamples)),ids=catalog.map(c=>c.id),groups=[...new Set(catalog.map(c=>c.group))];
assert.deepEqual(ids,manifest.ids);assert.deepEqual(groups,manifest.groups);assert.equal(manifest.examples,ids.length);
assert.deepEqual(manifest.missingGroups,manifest.requiredGroups.filter(g=>!groups.includes(g)));
assert.deepEqual(manifest.missingIds,manifest.requiredIds.filter(id=>!ids.includes(id)));
assert.equal(manifest.complete,manifest.missingGroups.length===0&&manifest.missingIds.length===0);
if(!partial){for(const id of manifest.requiredIds)assert.ok(ids.includes(id),'required release ID '+id);for(const group of manifest.requiredGroups)assert.ok(catalog.filter(c=>c.group===group).length>=2,'release group '+group);}
assert.equal(Object.keys(manifest.files).length,ids.length+groups.length+1);
for(const base of ['assets/animejs','dist/'+manifest.package])assert.deepEqual(fs.readdirSync(path.join(root,base),{recursive:true}).filter(p=>p.endsWith('.html')).sort(),Object.keys(manifest.files).sort(),base+' exact HTML inventory');
for(const [file,hash] of Object.entries(manifest.inputs))assert.equal(crypto.createHash('sha256').update(fs.readFileSync(path.join(root,file))).digest('hex'),hash,'Stale build input: '+file);
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'];
// Bind generated data to authored records, not merely to its own manifest.
const original={window:{}};vm.runInNewContext(read('assets/animejs/examples.js'),original);
for(const c of original.window.animeExamples){const built=catalog.find(x=>x.id===c.id);for(const [k,v] of Object.entries(c))assert.equal(JSON.stringify(built[k]),JSON.stringify(v),c.id+' legacy '+k);}
for(const c of catalog.filter(c=>c.kind!=='industry')){assert.equal(c.beatSource,'runtime');assert.equal(c.beats,undefined,'No stale legacy times in generated metadata');}
for(const group of groups){
const expected=catalog.filter(c=>c.group===group).map(c=>c.id).sort();
if(!['elements','compositions'].includes(group)){
const source=JSON.parse(read(`assets/animejs/catalogs/${group}.json`));assert.deepEqual(source.map(c=>c.id).sort(),expected);
for(const c of source)for(const [k,v] of Object.entries(c))assert.deepEqual(catalog.find(x=>x.id===c.id)[k],v,c.id+' '+k);
}
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(),expected,group+' registry IDs');
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);}
}
function sameVisual(actual,expected,message='SVG equality'){if(actual===expected)return;let i=0;while(i<Math.min(actual.length,expected.length)&&actual[i]===expected[i])i++;throw new Error(message+' at SVG offset '+i+'; actual '+JSON.stringify(actual.slice(Math.max(0,i-70),i+140))+'; expected '+JSON.stringify(expected.slice(Math.max(0,i-70),i+140)));}
// Independent semantic expectations, not the runtime's computed positions.
const readBeats=page=>page.locator('[data-beat]').evaluateAll(es=>es.map(e=>({at:Number(e.dataset.beat),label:e.textContent})));
function legacyBeats(c,total){
const labels={'svg-draw':['Source','Reveal connection','Connected'],'motion-path':['Send','In transit','Receive'],'spatial-stagger':['Grid ready','Center outward','Activated'],'dependency-flow':['Input','Check','Output'],'assemble-explode':['Assembled','Separate','Inspect hold','Reassembled']}[c.id];
const times=c.id==='assemble-explode'?[0,c.duration,c.duration+200,total]:[0,total/2,total];
return labels.map((label,i)=>({at:times[i],label}));
}
const legacyEdits={
'svg-draw':[{duration:450},{duration:1350}],
'motion-path':[{duration:480},{duration:1500}],
'spatial-stagger':[{duration:180,columns:4,rows:2,delay:65},{duration:520,columns:3,rows:4,delay:0},{duration:240,columns:5,rows:4,delay:140}],
'dependency-flow':[{duration:240},{duration:950}],
'assemble-explode':[{duration:180,distance:65},{duration:1100,distance:90}]
};
async function checkEditedLegacy(page,c,reducedMotion){
const {id}=c;
// Remount edited configs in the already-loaded source AND built page; no rebuild.
const expectedDuration=await page.evaluate(c=>{
const old=animeGallery.controllers[c.id];old.destroy();
animeGallery.controllers[c.id]=animeGallery.mount(document.getElementById(c.id),c);
if(c.id==='spatial-stagger'){
const cells=[...document.querySelectorAll('.cell')],delay=anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'});
return c.duration+Math.max(...cells.map((e,i)=>delay(e,i,cells)));
}
return c.id==='assemble-explode'?2*c.duration+400:c.id==='dependency-flow'?2*c.duration:c.duration;
},c);
const call=(method,arg)=>page.evaluate(({id,method,arg})=>animeGallery.controllers[id][method](arg),{id,method,arg});
const visual=()=>page.locator('svg').evaluate(e=>e.outerHTML),final=await visual(),s=await call('state'),beats=await readBeats(page);
assert.ok(Math.abs(s.duration-expectedDuration)<.01,id+' edited duration includes all delays/hold');
assert.deepEqual(beats,legacyBeats(c,s.duration),id+' edited beat semantics');
assert.ok(beats.every(b=>Number.isFinite(b.at)&&b.at>=0&&b.at<=s.duration));
if(id==='spatial-stagger')assert.equal(await page.locator('.cell').count(),c.columns*c.rows);
for(const beat of beats){
await page.getByRole('button',{name:beat.label,exact:true}).click();
assert.equal((await call('state')).time,reducedMotion==='reduce'?s.duration:beat.at);
if(reducedMotion==='reduce')sameVisual(await visual(),final);
}
if(reducedMotion==='no-preference'){
await call('seek',s.duration*.25);assert.notEqual(await visual(),final,id+' edited motion changes SVG');
if(id==='assemble-explode'){
const hold=beats.find(b=>b.label==='Inspect hold');
assert.ok(hold.at>c.duration&&hold.at<c.duration+400);
for(const from of [0,s.duration])for(const at of [c.duration+1,hold.at,c.duration+399]){
await call('seek',from);await call('seek',at);
assert.deepEqual(await page.locator('[data-part]').evaluateAll(es=>es.map(e=>new DOMMatrix(getComputedStyle(e).transform).m41)),[-c.distance,0,c.distance],'Fixed 400ms hold from both seek directions');
}
}
if(id==='dependency-flow'){
await call('seek',c.duration);const checked=await visual();await call('seek',s.duration);assert.notEqual(await visual(),checked,'Output connection draws after Check');
}
}
await call('replay');
if(reducedMotion==='no-preference'){
await page.waitForTimeout(70);const running=await call('state');assert.ok(running.time>0&&running.time<s.duration,id+' edited real playback');
}
await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
sameVisual(await visual(),final,id+' edited natural completion at speed 1');
if(id==='spatial-stagger')assert.ok(await page.locator('.cell').evaluateAll(es=>es.every(e=>Number(getComputedStyle(e).opacity)===1)),'Activated includes last staggered cell');
return {duration:s.duration,beats,playback:reducedMotion==='reduce'?'suppressed':'natural completion at speed 1'};
}
const evidence=path.join(root,'evidence');fs.mkdirSync(evidence,{recursive:true});
const report={package:manifest.package,completeCatalog:manifest.complete,missingGroups:manifest.missingGroups,missingIds:manifest.missingIds,examples:ids.length,groups:groups.length,htmlPages:Object.keys(manifest.files).length,files:manifest.files,playback:'Every normal-motion example: 120ms at engine speed 1, then natural completion at engine speed 12. Representative first legacy/industry complete at speed 1 on dist. Reduced motion settles without playback.',runs:[],findings:[],status:'running'};
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 ctx=await browser.newContext({offline:true,reducedMotion,viewport:{width:1280,height:1000}}),page=await ctx.newPage(),errors=[],network=[];
ctx.on('page',p=>p.on('pageerror',e=>errors.push(String(e))));page.on('pageerror',e=>errors.push(String(e)));page.on('console',m=>{if(m.type()==='error')errors.push(m.text());});ctx.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']){
const run={variant,reducedMotion,examples:[],hierarchy:false};report.runs.push(run);
for(const group of ['all',...groups]){
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);
const expected=catalog.filter(c=>group==='all'||c.group===group).map(c=>c.id);
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);
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());
assert.deepEqual(await inner.evaluate(()=>Object.keys(animeGallery.controllers)),[id]);assert.ok(await inner.locator('article h2').isVisible());assert.ok(await inner.locator('svg').isVisible());assert.equal(await inner.locator('.guidance').isVisible(),false);
assert.ok(await inner.evaluate(()=>document.documentElement.scrollHeight<=innerHeight+2),id+' preview overflow');
await inner.getByRole('button',{name:'Play',exact:true}).click();
if(reducedMotion==='no-preference'){await page.waitForTimeout(60);assert.ok(await inner.evaluate(id=>animeGallery.controllers[id].state().time>0,id),id+' embedded playback');}
await inner.getByRole('button',{name:'Settle',exact:true}).click();
assert.ok(await inner.evaluate(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id),id+' embedded settle');
}
if(group==='all')for(const g of groups)assert.equal(await page.locator(`section[data-registry="${g}"] h2 a`).evaluate(e=>e.href),url(g+'/index.html'));
if(group==='all'&&variant==='dist'&&reducedMotion==='no-preference'){
await page.evaluate(()=>window.scrollTo({top:0,left:0,behavior:'instant'}));
await page.waitForFunction(()=>scrollY===0);
assert.ok(await page.locator('.registry-nav').evaluate(e=>{const r=e.getBoundingClientRect();return r.top>=0&&r.bottom<=innerHeight;}),'Discovery navigation in overview');
await page.screenshot({path:path.join(evidence,'animejs-0.6.0-overview.png')});
}
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();
run.hierarchy=true;
for(const c of catalog){const {id}=c;try{
await page.goto(url('examples/'+id+'.html'));await page.waitForFunction(()=>window.animeGallery);
assert.deepEqual(await page.evaluate(()=>Object.keys(animeGallery.controllers)),[id]);assert.equal(await page.locator('h1').textContent(),c.title);
if(c.kind==='industry')assert.deepEqual(await page.evaluate(()=>Object.keys(industryRenderers).sort()),catalog.filter(x=>x.group===c.group).map(x=>x.id).sort(),'exact sector registrations');
assert.equal(await page.locator('svg').getAttribute('viewBox'),c.kind==='industry'?'0 0 960 540':'0 0 400 220');
assert.deepEqual(await page.locator('svg').evaluate(svg=>[...svg.querySelectorAll('text')].flatMap(e=>{const issues=[],style=getComputedStyle(e),size=e.getAttribute('font-size'),fill=e.getAttribute('fill');if(size&&/^[0-9.]+$/.test(size)&&parseFloat(style.fontSize)!==Number(size))issues.push('font-size overridden: '+e.textContent);if(fill&&fill!=='none'&&fill!=='currentColor'&&!fill.startsWith('url(')){const probe=document.createElement('span');probe.style.color=fill;document.body.append(probe);if(style.fill!==getComputedStyle(probe).color)issues.push('fill overridden: '+e.textContent);probe.remove();}return issues;})),[],id+' authored text styles');
for(const key of ['useWhen','avoid','assumptions'])assert.ok((await page.locator('.guidance').textContent()).includes(c[key]));
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);
const bounds=async()=>assert.deepEqual(await page.locator('svg').evaluate(svg=>{const r=svg.getBoundingClientRect();return [...svg.querySelectorAll('text,rect,circle,ellipse,path,line,polygon,polyline')].filter(e=>{for(let n=e;n&&n!==svg;n=n.parentElement){if(getComputedStyle(n).visibility==='hidden'||Number(getComputedStyle(n).opacity)===0)return false;}return true;}).filter(e=>{const b=e.getBoundingClientRect();return b.left<r.left-2||b.right>r.right+2||b.top<r.top-2||b.bottom>r.bottom+2;}).map(e=>e.tagName+':'+(e.textContent||e.getAttribute('d')||'').slice(0,70));}),[],id+' visible bounds');
const final=await visual(),s=await state();assert.ok(Number.isFinite(s.duration)&&s.duration>0);assert.equal(s.time,s.duration);await bounds();
const beats=await readBeats(page);
assert.deepEqual(beats,c.kind==='industry'?c.beats:legacyBeats(c,s.duration),id+' exact named beats');
for(const beat of beats)assert.ok(Number.isFinite(beat.at)&&beat.at>=0&&beat.at<=s.duration,id+' '+beat.label);
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);
const samples=[s.duration*.17,s.duration*.45,s.duration*.73,...beats.map(b=>b.at),...beats.slice(1).map((b,i)=>(b.at+beats[i].at)/2)];
for(const at of samples){await call('settle');await call('seek',at);const backward=await visual();await bounds();await call('seek',0);await call('seek',at);sameVisual(await visual(),backward,id+' deterministic seek at '+at);}
await call('seek',s.duration*.3);const earlier=await visual();await call('seek',s.duration*.8);assert.notEqual(await visual(),earlier,id+' changes with time');
if(id==='assemble-explode'){await call('seek',900);assert.deepEqual(await page.locator('[data-part]').evaluateAll(parts=>parts.map(e=>new DOMMatrix(getComputedStyle(e).transform).m41)),[-105,0,105]);}
for(const beat of beats){await page.locator(`[data-beat="${beat.at}"]`).click();assert.equal((await state()).time,beat.at);}
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);
}else{await call('seek',0);sameVisual(await visual(),final);await page.locator('[data-beat]').first().click();sameVisual(await visual(),final);}
await call('settle');await page.locator('[data-action="play"]').focus();await page.keyboard.press('Enter');
if(reducedMotion==='no-preference'){
await page.waitForTimeout(120);const running=await state();assert.ok(running.time>0&&running.time<running.duration,id+' early playback');
await page.evaluate(()=>{anime.engine.speed=12;});
}
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'});
await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);sameVisual(await visual(),final,id+' natural completion');
await page.evaluate(()=>{anime.engine.speed=1;});
await page.locator('[data-action="replay"]').focus();await page.keyboard.press('Space');
if(variant==='dist'&&reducedMotion==='no-preference'&&(id===ids[0]||id===catalog.find(x=>x.kind==='industry')?.id))await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
await page.locator('[data-action="settle"]').click();sameVisual(await visual(),final);
if(variant==='dist'&&reducedMotion==='no-preference'&&(id==='assemble-explode'||id===catalog.find(x=>x.kind==='industry')?.id)){
await page.locator('article').screenshot({path:path.join(evidence,`animejs-0.6.0-${id}-final.png`)});await call('seek',s.duration*.45);await page.locator('article').screenshot({path:path.join(evidence,`animejs-0.6.0-${id}-mid.png`)});
}
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.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+beats.length);sameVisual(await visual(),final,id+' remount');
await page.emulateMedia({reducedMotion});
if(c.kind!=='industry')for(const edits of legacyEdits[id]){
const config={...c,...edits};
const result=await checkEditedLegacy(page,config,reducedMotion);
(run.legacyConfigurations??=[]).push({id,edits,...result});
}
run.examples.push(id);
}catch(error){report.findings.push({variant,reducedMotion,id,error:String(error.stack)});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();}
assert.deepEqual(errors,[]);assert.deepEqual(network,[]);Object.assign(run,{passed:run.examples.length===ids.length,checksApplyTo:'Successful IDs in examples; failures in findings',deterministicSeek:reducedMotion==='no-preference',cleanupRemount:true,beatControls:true,bounds:true,errors,network});await ctx.close();
}assert.equal(report.findings.length,0,'Recipe integration findings: '+report.findings.map(f=>f.variant+'/'+f.id).join(', ')+' (see evidence report)');report.status='passed';console.log(JSON.stringify({status:report.status,examples:ids.length,runs:report.runs.map(r=>({variant:r.variant,motion:r.reducedMotion,examples:r.examples.length})),completeCatalog:manifest.complete}));}
catch(error){report.status='failed';report.failure=String(error.stack);throw error;}
finally{fs.writeFileSync(path.join(evidence,'animejs-gallery-test-report.json'),JSON.stringify(report,null,2)+'\n');await browser.close();}