feat: add verified deterministic Anime v4 choreography
This commit is contained in:
1 parent
563aaf4757
commit
6854138486
19 files changed
+493
-28
No files matched your search
+1
-1
@@ -24,7 +24,7 @@ def build():
|
||||
s=s.replace('/* FONT_CSS */',fonts).replace('/* APP_JS */',(R/'assets/app.js').read_text())
|
||||
licenses='\n'.join((vendor/f).read_text() for f in ['reveal-LICENSE','anime-LICENSE.md','work-sans-LICENSE'])
|
||||
s=s.replace('<!-- LICENSES -->','<!-- THIRD-PARTY NOTICES\n'+licenses.replace('--','—')+'\n-->')
|
||||
out=R/'dist';out.mkdir(exist_ok=True);p=out/'presentation-html-demo-0.1.0.html';p.write_text(s)
|
||||
out=R/'dist';out.mkdir(exist_ok=True);p=out/'presentation-html-demo-0.3.0.html';p.write_text(s)
|
||||
report={'file':p.name,'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'bytes':p.stat().st_size,'slides':6,'interactive_nodes':6,'reveal':'6.0.2','anime':'4.5.0'}
|
||||
(out/'manifest.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report))
|
||||
if __name__=='__main__':build()
|
||||
@@ -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(); }
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
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,'..');const file=process.env.DECK_PATH||path.join(root,'dist/presentation-html-demo-0.1.0.html');
|
||||
const root=path.resolve(import.meta.dirname,'..');const file=process.env.DECK_PATH||path.join(root,'dist/presentation-html-demo-0.3.0.html');
|
||||
const executablePath=process.env.CHROMIUM_EXECUTABLE;
|
||||
const browser=await chromium.launch({headless:true,...(executablePath?{executablePath}:{})});
|
||||
const report={file,sha256:crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'),runs:[]};
|
||||
|
||||
Reference in new issue
Block a user