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{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();}