Files
2026-10-27 17:39:27 +07:00

92 lines
5.9 KiB
JavaScript

import {routingModuleUrl} from './helpers/routing.mjs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import ts from 'typescript';
async function moduleUrl(file,replace=code=>code){
const filename=new URL(file,import.meta.url);
const {outputText}=ts.transpileModule(await readFile(filename,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}});
return 'data:text/javascript;base64,'+Buffer.from(replace(outputText).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))+'\n//# sourceURL='+filename.pathname).toString('base64');
}
const protocol=await moduleUrl('../src/lib/api/protocol.ts');
const render=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocol)));
const apps=await moduleUrl('../src/lib/client/apps.ts',code=>code.replace("'../api/protocol'",JSON.stringify(protocol)).replace("'./render'",JSON.stringify(render)));
const {loadAppCategories,renderAppDirectory,scrollToAppCategory}=await import(apps);
const app=id=>({id,name:'应用 '+id,url:'https://example.com/app/'+id,logo:'https://example.com/icon.png'});
const category=(id,appList)=>({id,title:'分类 '+id,...(appList===undefined?{}:{appList})});
test('benefit applications mark only linked advertisements and carry no tracking attributes',()=>{
const html=renderAppDirectory([category(3,[app(11),{...app(12),advitiseId:'ad-12',location:8}])]);
assert.equal(html.match(/data-ad /g)?.length,1);
assert.doesNotMatch(html,/data-track|data-item-|data-ad-/);
});
test('loads every missing category once, preserves directory order, and omits empty categories',async()=>{
const calls=[],controller=new AbortController();
const api={async get(path,query,signal){
assert.equal(path,'applist/list');assert.equal(signal,controller.signal);calls.push(query.categoryId);
if(query.categoryId===0)return {categoryList:[category(2,[]),category(1),category(3,[]),category(4),category(2)],category:category(1,[app(11)])};
return {category:category(query.categoryId,query.categoryId===2?[app(22)]:[])};
}};
const result=await loadAppCategories(api,controller.signal);
assert.deepEqual(calls,[0,2,3,4]);assert.deepEqual(result.map(item=>item.id),[2,1]);
const html=renderAppDirectory(result);
for(const id of [2,1]){assert.ok(html.includes(`aria-controls="apps-${id}"`));assert.ok(html.includes(`id="apps-${id}"`));}
for(const id of [3,4]){assert.ok(!html.includes(`data-app-category="${id}"`));assert.ok(!html.includes(`id="apps-${id}"`));}
assert.equal((html.match(/class="benefit-section"/g)||[]).length,2);
});
test('uses embedded applications without additional requests and escapes API text and links',async()=>{
let calls=0;
const result=await loadAppCategories({async get(){calls++;return {categoryList:[{...category(7,[{...app(1),name:'应用 <script>',url:'https://example.com/?a=1&b=2'}]),title:'分类 <img>'},category(8,[app(2)])]};}},new AbortController().signal);
assert.equal(calls,1);
const html=renderAppDirectory(result);
assert.ok(html.includes('分类 &lt;img&gt;'));assert.ok(html.includes('应用 &lt;script&gt;'));assert.ok(html.includes('?a=1&amp;b=2'));
assert.ok(html.includes('data-src="https://example.com/icon.png"'));assert.ok(!html.includes('<script>'));
});
test('empty and malformed application lists create no category options or empty sections',async()=>{
for(const data of [null,{}, {categoryList:null},{categoryList:[null,{},category(1,[]),category(2,[null,{},false]),category(3,'invalid')]}]){
const result=await loadAppCategories({async get(){return data;}},new AbortController().signal);
assert.deepEqual(result,[]);
const html=renderAppDirectory(result);assert.ok(html.includes('暂无福利应用'));
assert.ok(!html.includes('data-app-category'));assert.ok(!html.includes('benefit-section'));
}
});
test('keeps available groups after a category failure and reports failure when none can load',async()=>{
const unavailable=new Error('category unavailable');
for(const available of [true,false]){
const api={async get(_,query){
if(query.categoryId===0)return {categoryList:[category(1),category(2)],category:available?category(1,[app(1)]):undefined};
throw unavailable;
}};
if(available)assert.deepEqual((await loadAppCategories(api,new AbortController().signal)).map(item=>item.id),[1]);
else await assert.rejects(loadAppCategories(api,new AbortController().signal),unavailable);
}
});
test('aborted navigation discards late category results',async()=>{
const controller=new AbortController();let finish;
const result=loadAppCategories({async get(_,query){
if(query.categoryId===0)return {categoryList:[category(1)]};
return new Promise(resolve=>{finish=()=>resolve({category:category(1,[app(1)])});});
}},controller.signal);
await new Promise(resolve=>setImmediate(resolve));controller.abort();finish();
assert.deepEqual(await result,[]);
});
test('category selection scrolls to existing sections and updates selection without replacing content',()=>{
const previousWindow=globalThis.window,scrolls=[];
const buttons=[1,2].map(id=>({dataset:{appCategory:String(id)},setAttribute(key,value){this[key]=value;}}));
const root={querySelector:selector=>selector==='#apps-2'?{scrollIntoView:options=>scrolls.push(options)}:null,querySelectorAll:()=>buttons};
try{
globalThis.window={matchMedia:()=>({matches:false})};
scrollToAppCategory(root,'2');assert.deepEqual(scrolls,[{behavior:'smooth',block:'start'}]);
assert.equal(buttons[0]['aria-pressed'],'false');assert.equal(buttons[1]['aria-pressed'],'true');
globalThis.window.matchMedia=()=>({matches:true});scrollToAppCategory(root,'2');assert.equal(scrolls[1].behavior,'auto');
scrollToAppCategory(root,'3');scrollToAppCategory(root,'invalid');assert.equal(scrolls.length,2);
}finally{globalThis.window=previousWindow;}
});