Files
web_hgcg_h5/tests/hero-fade.test.mjs
谢宇宁andClaude Opus 5.5 8cdbfee08f Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:23:48 +07:00

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);
});