feat: add tested Anime.js example registries and offline gallery
This commit is contained in:
1 parent
6854138486
commit
55e559dcd1
17 files changed
+388
-6
No files matched your search
@@ -0,0 +1,27 @@
|
||||
"""Dependency-free, offline gallery packager. Run from any directory."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = root / 'assets/animejs'
|
||||
vendor = root / 'assets/vendor/anime.umd.min.js'
|
||||
entries = [entry for entry in json.loads((root / 'vendor-manifest.json').read_text(encoding='utf-8'))
|
||||
if entry['path'] == 'assets/vendor/anime.umd.min.js']
|
||||
if len(entries) != 1 or hashlib.sha256(vendor.read_bytes()).hexdigest() != entries[0]['sha256']:
|
||||
raise ValueError('Missing, duplicate or mismatched Anime vendor fingerprint')
|
||||
text = (source / 'index.html').read_text()
|
||||
text = text.replace('<link rel="stylesheet" href="gallery.css">', '<style>' + (source / 'gallery.css').read_text() + '</style>')
|
||||
for name, file in [('../vendor/anime.umd.min.js', vendor), ('examples.js', source / 'examples.js'), ('gallery.js', source / 'gallery.js')]:
|
||||
js = re.sub(r'//# sourceMappingURL=.*', '', file.read_text())
|
||||
text = text.replace(f'<script src="{name}"></script>', '<script>' + js.replace('</script', r'<\/script') + '</script>')
|
||||
text = text.replace('ANIME_LICENSE', html.escape((root / 'assets/vendor/anime-LICENSE.md').read_text()))
|
||||
out = root / 'dist/animejs-gallery-0.4.0.html'
|
||||
out.parent.mkdir(exist_ok=True)
|
||||
out.write_text(text)
|
||||
inputs = [*sorted(source.glob('*')), vendor, root / 'assets/vendor/anime-LICENSE.md']
|
||||
report = {'file': out.name, 'sha256': hashlib.sha256(out.read_bytes()).hexdigest(), 'anime':'4.5.0', 'examples':5, 'inputs':{str(p.relative_to(root)):hashlib.sha256(p.read_bytes()).hexdigest() for p in inputs if p.is_file()}}
|
||||
(root / 'dist/animejs-gallery-manifest.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print(json.dumps(report, indent=2))
|
||||
@@ -0,0 +1,60 @@
|
||||
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<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);
|
||||
}
|
||||
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();}
|
||||
Reference in new issue
Block a user