92 lines
6.1 KiB
JavaScript
92 lines
6.1 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 report linked advertisements with their API ids and ordinary applications as content',()=>{
|
|
const html=renderAppDirectory([category(3,[app(11),{...app(12),advitiseId:'ad-12',location:8}])]);
|
|
assert.match(html,/data-track-item data-item-id="11" data-item-name="应用 11" data-item-type="app" data-parent-item-id="3"/);
|
|
assert.match(html,/data-track-ad="external" data-ad-id="ad-12" data-ad-clickable="1" data-ad-slot-key="8" data-ad-slot-name="福利应用" data-ad-type="icon"/);
|
|
});
|
|
|
|
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('分类 <img>'));assert.ok(html.includes('应用 <script>'));assert.ok(html.includes('?a=1&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;}
|
|
});
|