feat: add verified deterministic Anime v4 choreography

This commit is contained in:
jarvis-at-skic committed 2026-10-23 09:40:36 +00:00
1 parent 563aaf4757
commit 6854138486
19 files changed
+493 -28

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
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 browser=await chromium.launch({headless:true,...(process.env.CHROMIUM_EXECUTABLE?{executablePath:process.env.CHROMIUM_EXECUTABLE}:{})});
try {
const context=await browser.newContext({offline:true});
const page=await context.newPage();const errors=[],requests=[];
page.on('pageerror',e=>errors.push(String(e)));
page.on('request',r=>{if(/^https?:/.test(r.url()))requests.push(r.url());});
await page.goto(pathToFileURL(path.join(root,'dist/presentation-html-demo-0.3.0.html')).href);
await page.waitForFunction(()=>window.deckReady);
assert.equal(await page.evaluate(()=>typeof presentationQA.seek),'function');
await page.evaluate(()=>Reveal.slide(3));
const sample=()=>page.evaluate(()=>[...document.querySelectorAll('#sequence [data-motion]')].map(e=>({opacity:e.style.opacity,transform:e.style.transform})));
await page.evaluate(()=>presentationQA.seek(240)); const first=await sample();
await page.evaluate(()=>presentationQA.seek(600));await page.evaluate(()=>presentationQA.seek(240));
assert.deepEqual(await sample(),first,'backward seek is deterministic');
assert.equal(await page.evaluate(()=>presentationQA.active()),0,'seek pauses the clock');
await page.evaluate(()=>presentationQA.seek(Infinity)).then(()=>assert.fail('nonfinite seek accepted'),()=>{});
await page.evaluate(()=>presentationQA.replay());
assert.equal(await page.evaluate(()=>presentationQA.active()),1);
await page.evaluate(()=>Reveal.slide(4));
assert.equal(await page.evaluate(()=>[...document.querySelectorAll('#sequence [data-motion]')].every(e=>+getComputedStyle(e).opacity===1)),true,'departure settles previous slide');
await page.evaluate(()=>Reveal.slide(3));assert.equal(await page.evaluate(()=>presentationQA.active()),0,'revisit is complete');
await page.evaluate(()=>presentationQA.replay());await page.keyboard.press('Escape');
assert.equal(await page.evaluate(()=>presentationQA.active()),0,'overview settles');await page.keyboard.press('Escape');
await page.evaluate(()=>presentationQA.replay());await page.emulateMedia({reducedMotion:'reduce'});
await page.waitForFunction(()=>presentationQA.active()===0 && document.querySelector('#motion').getAttribute('aria-pressed')==='false');
assert.equal(await page.evaluate(()=>presentationQA.active()),0);
assert.equal(await page.evaluate(()=>presentationQA.replay()),false,'reduced motion forbids replay');
await page.evaluate(()=>presentationQA.seek(0));
assert.equal(await page.evaluate(()=>[...document.querySelectorAll('#sequence [data-motion]')].every(e=>+getComputedStyle(e).opacity===1)),true,'seek respects reduced motion');
await page.emulateMedia({reducedMotion:'no-preference'});
await page.evaluate(()=>presentationQA.replay());await page.evaluate(()=>dispatchEvent(new Event('beforeprint')));
assert.equal(await page.evaluate(()=>presentationQA.active()),0);
await page.setContent('<svg width="240" height="80" viewBox="0 0 240 80"><path id="flow-path" d="M0 30 L200 30" fill="none" stroke="black"/><circle id="packet" r="5"/></svg><div id="cells">'+ '<i style="display:inline-block;width:20px;height:20px">•</i>'.repeat(6)+'</div>');
await page.addScriptTag({path:path.join(root,'assets/vendor/anime.umd.min.js')});
const primitives=await page.evaluate(()=>{
const edge=document.querySelector('#flow-path');
const trace=anime.animate(anime.svg.createDrawable(edge),{draw:['0 0','0 1'],duration:600,ease:'linear',autoplay:false});
const flow=anime.animate(document.querySelector('#packet'),{...anime.svg.createMotionPath(edge),duration:600,ease:'linear',autoplay:false});
const pulse=anime.animate(document.querySelectorAll('#cells i'),{opacity:[.25,1],delay:anime.stagger(60,{grid:[3,2],from:'center'}),duration:240,ease:'linear',autoplay:false});
const snapshot=()=>[edge.getAttribute('stroke-dasharray'),edge.getAttribute('stroke-dashoffset'),document.querySelector('#packet').getAttribute('style'),...Array.from(document.querySelectorAll('#cells i'),e=>e.style.opacity)];
[trace,flow,pulse].forEach(a=>a.seek(200));const first=snapshot();
[trace,flow,pulse].forEach(a=>a.seek(a.duration));const end=snapshot();
[trace,flow,pulse].forEach(a=>a.seek(200));const second=snapshot();
const x=new DOMMatrix(getComputedStyle(document.querySelector('#packet')).transform).m41;
return {first,end,second,x,paused:[trace,flow,pulse].every(a=>a.paused)};
});
assert.deepEqual(primitives.first,primitives.second);assert.notDeepEqual(primitives.first,primitives.end);
assert.notDeepEqual(primitives.first.slice(0,2),primitives.end.slice(0,2),'SVG draw changes dash attributes');
assert.ok(primitives.first.slice(3).some(v=>+v<1),'grid has intermediate opacity');
assert.ok(primitives.end.slice(3).every(v=>+v===1),'grid final state is visible');
assert.equal(primitives.paused,true);assert.ok(primitives.x>50&&primitives.x<80,'motion path reaches one-third of 200px path: '+JSON.stringify(primitives));
assert.deepEqual(errors,[]);assert.deepEqual(requests,[]);
fs.writeFileSync(path.join(root,'evidence/motion-test-report.json'),JSON.stringify({passed:true,checks:['deterministic seek','finite input','replay','departure','revisit','overview','reduced motion','print','SVG draw','SVG motion path','grid stagger'],errors,requests},null,2)+'\n');
console.log('Motion lifecycle and v4 SVG/grid recipes: PASS');
} finally { await browser.close(); }