84 lines
4.8 KiB
JavaScript
84 lines
4.8 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFile} from 'node:fs/promises';
|
|
import vm from 'node:vm';
|
|
import ts from 'typescript';
|
|
import {seo} from './helpers/seo.mjs';
|
|
|
|
const sourceUrl=new URL('../src/lib/client/application.ts',import.meta.url);
|
|
const {outputText}=ts.transpileModule(await readFile(sourceUrl,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.CommonJS}});
|
|
const flush=()=>new Promise(resolve=>setImmediate(resolve));
|
|
|
|
function fixture({reducedMotion=false,load=async()=>{}}={}){
|
|
const animations=[],overlays=[],nodes=new Map();
|
|
const target={isConnected:true,current:0,replaceChildren(frame){this.current=frame.selected;},append(node){overlays.push(node);},cloneNode(){
|
|
return {current:this.current,inert:false,classList:{add(){}},removeAttribute(){},setAttribute(){},querySelectorAll:()=>[],
|
|
remove(){const index=overlays.indexOf(this);if(index>=0)overlays.splice(index,1);},
|
|
animate(frames,options){
|
|
let finish,reject;
|
|
const animation={frames,options,finished:new Promise((done,fail)=>{finish=done;reject=fail;}),finish:()=>finish(),cancel(){reject(new Error('cancelled'));}};
|
|
animations.push(animation);return animation;
|
|
}};
|
|
}};
|
|
const document={querySelector(selector){
|
|
if(selector==='#hero')return target;
|
|
if(!nodes.has(selector))nodes.set(selector,{});return nodes.get(selector);
|
|
},querySelectorAll:()=>[],createElement(){
|
|
return {set innerHTML(value){this.selected=Number(value);this.childNodes=[{selected:this.selected}];},querySelector(){
|
|
const source='image-'+this.selected;
|
|
return {getAttribute:()=>source,decode:async()=>{}};
|
|
}};
|
|
}};
|
|
const exports={};
|
|
const require=specifier=>{
|
|
if(specifier==='./api')return {ApiClient:class {}};
|
|
if(specifier==='./render')return {activeBanners:home=>home.homeBannerData,hero:(_,selected)=>String(selected),empty:()=>'',escape:String};
|
|
if(specifier==='./images')return {processImage:load};
|
|
if(specifier==='../routing')return {parseSitePath:()=>null,topNavigationSection:()=>''};
|
|
if(specifier==='../seo')return seo;
|
|
if(specifier==='./seo')return {applyPageHead:head=>{document.title=head.title;}};
|
|
return {};
|
|
};
|
|
vm.runInNewContext(outputText,{exports,require,AbortController,document,window:{matchMedia:()=>({matches:reducedMotion}),scrollTo(){}},location:{pathname:'/missing/',search:''},clearInterval(){}},{filename:sourceUrl.pathname});
|
|
const app=new exports.Application({homeBannerData:[{id:1},{id:2},{id:3}]});
|
|
return {app,target,animations,overlays};
|
|
}
|
|
|
|
test('keeps the old frame until its replacement image is ready, then fades it out and removes it',async()=>{
|
|
let ready;
|
|
const f=fixture({load:()=>new Promise(resolve=>ready=resolve)});
|
|
const changing=f.app.selectHero(1);
|
|
assert.equal(f.target.current,0);assert.equal(f.overlays.length,0);
|
|
ready();await changing;
|
|
assert.equal(f.target.current,1);assert.equal(f.overlays[0].current,0);assert.equal(f.overlays[0].inert,true);
|
|
assert.equal(f.animations[0].options.duration,500);
|
|
assert.deepEqual(Array.from(f.animations[0].frames,frame=>frame.opacity),[1,0]);
|
|
f.animations[0].finish();await flush();assert.equal(f.overlays.length,0);
|
|
});
|
|
|
|
test('rapid switches keep the latest request and replace any unfinished fade without stacking snapshots',async()=>{
|
|
const pending=new Map();
|
|
const f=fixture({load:source=>new Promise(resolve=>pending.set(source,resolve))});
|
|
const first=f.app.selectHero(1),second=f.app.selectHero(2);
|
|
pending.get('image-2')();await second;assert.equal(f.target.current,2);
|
|
pending.get('image-1')();await first;assert.equal(f.target.current,2);assert.equal(f.overlays.length,1);
|
|
const third=f.app.selectHero(0);pending.get('image-0')();await third;await flush();
|
|
assert.equal(f.target.current,0);assert.equal(f.overlays.length,1);assert.equal(f.overlays[0].current,2);
|
|
f.animations.at(-1).finish();await flush();assert.equal(f.overlays.length,0);
|
|
});
|
|
|
|
test('document disposal cancels pending image commits and clean up a running fade',async()=>{
|
|
let ready;
|
|
const f=fixture({load:()=>new Promise(resolve=>ready=resolve)});
|
|
const changing=f.app.selectHero(1);f.app.disposePage();ready();await changing;
|
|
assert.equal(f.target.current,0);assert.equal(f.animations.length,0);assert.equal(f.app.feature,0);
|
|
const running=fixture();await running.app.selectHero(1);assert.equal(running.overlays.length,1);
|
|
running.app.disposePage();await flush();assert.equal(running.overlays.length,0);
|
|
});
|
|
|
|
test('reduced motion switches directly, and selecting the current slide leaves the frame intact',async()=>{
|
|
const f=fixture({reducedMotion:true});
|
|
await f.app.selectHero(0);assert.equal(f.target.current,0);assert.equal(f.animations.length,0);
|
|
await f.app.selectHero(1);assert.equal(f.target.current,1);assert.equal(f.animations.length,0);assert.equal(f.overlays.length,0);
|
|
});
|