Files
web_hgcg_h5/tests/promo-marquee.test.mjs
T
谢宇宁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

95 lines
6.3 KiB
JavaScript

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import ts from 'typescript';
import {setMaxListeners} from 'node:events';
const code=ts.transpileModule(await readFile(new URL('../src/lib/client/promo-marquee.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const {bindPromotionMarquee}=await import('data:text/javascript;base64,'+Buffer.from(code).toString('base64'));
class Element extends EventTarget {
attributes=new Map();children=[];tabIndex=0;
setAttribute(key,value){this.attributes.set(key,value);}
hasAttribute(key){return this.attributes.has(key);}
removeAttribute(key){this.attributes.delete(key);}
remove(){if(this.parent)this.parent.children=this.parent.children.filter(child=>child!==this);}
append(child){this.children.push(child);child.parent=this;}
querySelectorAll(selector){return selector==='[id]'?this.children.filter(child=>child.hasAttribute('id')):this.children;}
cloneNode(){const copy=new Element();for(const child of this.children){const link=new Element();link.attributes=new Map(child.attributes);copy.append(link);}return copy;}
}
function event(target,type,values={}){const e=new Event(type);for(const [key,value] of Object.entries(values))Object.defineProperty(e,key,{value});target.dispatchEvent(e);}
function fixture({width=800,viewport=500,reduced=false}={}){
const previous=globalThis.ResizeObserver,frames=new Map(),timers=new Map();let sequence=0,observer;
const root=new Element(),group=new Element(),track=new Element(),link=new Element(),doc=new EventTarget(),view=new EventTarget(),motion=new EventTarget();
link.setAttribute('id','original-ad');group.append(link);track.append(group);
root.clientWidth=viewport;root.scrollLeft=0;root.isConnected=true;
root.querySelector=selector=>selector==='.promo-group'?group:track;
root.contains=node=>node===link||track.children.some(child=>child.children.includes(node));
group.getBoundingClientRect=()=>({width:width+(root.hasAttribute('data-looping')?14:0)});
doc.hidden=false;doc.activeElement=null;doc.defaultView=view;root.ownerDocument=doc;
motion.matches=reduced;view.matchMedia=()=>motion;
view.getComputedStyle=()=>({paddingRight:root.hasAttribute('data-looping')?'14px':'0px'});
view.requestAnimationFrame=callback=>{const id=++sequence;frames.set(id,callback);return id;};
view.cancelAnimationFrame=id=>frames.delete(id);
view.setTimeout=callback=>{const id=++sequence;timers.set(id,callback);return id;};view.clearTimeout=id=>timers.delete(id);
globalThis.ResizeObserver=class {constructor(callback){this.callback=callback;this.disconnects=0;observer=this;}observe(){}disconnect(){this.disconnects++;}};
const controller=new AbortController();setMaxListeners(30,controller.signal);
const dispose=bindPromotionMarquee(root,controller.signal);
return {root,group,track,link,doc,view,motion,frames,timers,controller,
tick(time){const callbacks=[...frames.values()];frames.clear();for(const callback of callbacks)callback(time);},
settle(){const callbacks=[...timers.values()];timers.clear();for(const callback of callbacks)callback();},
resize(value){root.clientWidth=value;observer.callback();},
get observer(){return observer;},
close(){dispose();if(previous===undefined)delete globalThis.ResizeObserver;else globalThis.ResizeObserver=previous;},
};
}
test('a fitting row stays still; overflow makes one seamless clickable copy and resizing removes it',()=>{
const f=fixture({viewport:900});
try{
assert.equal(f.track.children.length,1);assert.equal(f.frames.size,0);
f.resize(500);assert.equal(f.track.children.length,2);assert.equal(f.frames.size,1);
const copy=f.track.children[1];assert.equal(copy.attributes.get('aria-hidden'),'true');assert.equal(copy.children[0].tabIndex,-1);assert.equal(copy.children[0].hasAttribute('id'),false);
f.resize(499);assert.equal(f.track.children.length,2);assert.equal(f.frames.size,1);
f.tick(0);f.tick(100);assert.ok(Math.abs(f.root.scrollLeft-3.2)<0.001);
event(f.root,'wheel');f.root.scrollLeft=813;f.settle();f.tick(200);f.tick(300);
assert.ok(Math.abs(f.root.scrollLeft-2.2)<0.001,'scrolling must wrap by exactly the original group including its trailing gap');
f.resize(1000);assert.equal(f.track.children.length,1);assert.equal(f.frames.size,0);assert.equal(f.root.hasAttribute('data-looping'),false);
}finally{f.close();}
});
test('mouse hover and keyboard focus pause the row and resume from the existing position',()=>{
const f=fixture();
try{
f.tick(0);f.tick(100);const position=f.root.scrollLeft;
event(f.root,'pointerenter',{pointerType:'mouse'});assert.equal(f.frames.size,0);
event(f.root,'pointerleave',{pointerType:'mouse'});assert.equal(f.frames.size,1);f.tick(10000);assert.equal(f.root.scrollLeft,position);
f.doc.activeElement=f.link;event(f.root,'focusin');assert.equal(f.frames.size,0);
event(f.root,'focusout',{relatedTarget:null});f.doc.activeElement=null;f.settle();assert.equal(f.frames.size,1);
}finally{f.close();}
});
test('touch momentum finishes before auto-scrolling resumes, without changing the manually selected ad',()=>{
const f=fixture();
try{
event(f.root,'pointerdown',{pointerType:'touch'});assert.equal(f.frames.size,0);
f.root.scrollLeft=937.5;event(f.view,'pointerup');assert.equal(f.timers.size,1);
event(f.root,'scroll');assert.equal(f.timers.size,1);
f.settle();f.tick(0);f.tick(100);
assert.ok(Math.abs(f.root.scrollLeft-126.7)<0.001);assert.equal(f.frames.size,1);
}finally{f.close();}
});
test('reduced motion, hidden pages and route cancellation stop animation and clean up listeners and duplicate links',()=>{
const f=fixture({reduced:true});
try{
assert.equal(f.track.children.length,1);assert.equal(f.frames.size,0);
f.motion.matches=false;event(f.motion,'change');assert.equal(f.track.children.length,2);
f.doc.hidden=true;event(f.doc,'visibilitychange');assert.equal(f.frames.size,0);
f.doc.hidden=false;event(f.doc,'visibilitychange');assert.equal(f.frames.size,1);
f.controller.abort();assert.equal(f.frames.size,0);assert.equal(f.track.children.length,1);assert.equal(f.observer.disconnects,1);
event(f.root,'pointerdown');event(f.view,'pointerup');assert.equal(f.timers.size,0,'cancelled routes must remove interaction handlers');
}finally{f.close();}
});