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('
'+ '•'.repeat(6)+'
'); 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(); }