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 routingUrl=await moduleUrl('../src/lib/routing.ts');
const {routeFromSegments,parseSitePath,paginationPath,internalPagePath,searchPath}=await import(routingUrl);
const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts');
const renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)));
const {pagination}=await import(renderUrl);
const {bindPaginationJumps}=await import(await moduleUrl('../src/lib/client/navigation.ts'));
const authUrl=await moduleUrl('../src/lib/client/auth.ts');
const trackingUrl=await moduleUrl('../src/lib/tracking.ts');
const accountUrl=await moduleUrl('../src/lib/client/account.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)).replace('"./render"',JSON.stringify(renderUrl)).replace("'./auth'",JSON.stringify(authUrl)).replace("'../tracking'",JSON.stringify(trackingUrl)));
const {renderMember,renderWallet}=await import(accountUrl);
test('every paginated page type resolves directly from the page suffix',()=>{
const keyword='都市 / page/7/?#%2F';
const bases=['/shorts/','/topics/','/topics/7/','/theme/8/','/tag/9/','/config/31/','/rank/','/rank/hot/','/rank/new/','/rank/follow/','/search/'+encodeURIComponent(keyword)+'/', '/member/favorites/','/member/history/','/member/likes/','/member/purchased/','/info/deposit/','/info/deposit/vip/','/detail/23456/','/play/23456/90001/'];
for(const base of bases){
for(const page of [1,2,37]){
const path=base+'page/'+page+'/';
assert.deepEqual(parseSitePath(path),{...parseSitePath(base),page},path);
assert.deepEqual(parseSitePath(path.slice(0,-1)),parseSitePath(path),path);
assert.equal(internalPagePath(path,'https://drama.example/'),path,path);
assert.equal(paginationPath(path,1),base.startsWith('/search/')?searchPath(keyword):base,path);
}
}
assert.deepEqual(routeFromSegments(['search',keyword,'page','3']),{group:'search',key:keyword,page:3});
});
test('invalid page numbers, nested page suffixes, and non-paginated sections are rejected',()=>{
for(const value of ['0','-1','1.5','02','NaN','9007199254740992'])assert.equal(parseSitePath(`/shorts/page/${value}/`),null,value);
for(const base of ['/','/apps/','/home-road/','/navigation/','/member/center/','/member/settings/','/info/about-us/'])assert.equal(parseSitePath(base+'page/2/'),null,base);
assert.equal(parseSitePath('/shorts/page/2/page/3/'),null);
assert.throws(()=>paginationPath('/shorts/',0));assert.throws(()=>paginationPath('/apps/',2));
});
test('page links retain filters and encoded search terms and sorting resets to the first page',()=>{
assert.equal(paginationPath('/shorts/page/4/?sort=3&source=menu',2),'/shorts/page/2/?sort=3&source=menu');
assert.equal(paginationPath('/shorts/page/4/?sort=3&source=menu',1,{sort:2}),'/shorts/?sort=2&source=menu');
assert.equal(paginationPath('/shorts/page/4/?sort=3',1,{sort:null}),'/shorts/');
const base='/search/'+encodeURIComponent('都市 / 爱情?#%')+'/';
assert.equal(paginationPath(base,2),searchPath('都市 / 爱情?#%',2));
const url=new URL(paginationPath(base,2),'https://drama.example');
assert.equal(parseSitePath(url.pathname,url.search).key,'都市 / 爱情?#%');
assert.equal(paginationPath('/play/7/90001/page/3/?commentSort=1',1,{commentSort:null}),'/play/7/90001/');
});
test('pagination renders real page links with correct boundaries, current page, and bounded page numbers',()=>{
const href=page=>paginationPath('/rank/new/page/5/?sort=2&from=menu',page);
const html=pagination(240,5,href);
assert.ok(html.includes('href="/rank/new/page/4/?sort=2&from=menu" rel="prev"'));
assert.ok(html.includes('href="/rank/new/page/6/?sort=2&from=menu" rel="next"'));
assert.ok(html.includes('href="/rank/new/page/5/?sort=2&from=menu" aria-current="page"'));
assert.equal((html.match(/'));assert.ok(!html.includes('data-page-number'));
assert.ok(html.includes('data-page-base="/rank/new/?sort=2&from=menu"'));
assert.ok(html.includes('name="page" min="1" max="10" step="1"'));
assert.ok((html.match(/{
const previousForm=globalThis.HTMLFormElement,previousInput=globalThis.HTMLInputElement;
class Input {valueAsNumber=3;max='10';}
class Form {dataset={pageBase:'/topics/sweet-love/?sort=2&source=menu'};input=new Input();valid=true;elements={namedItem:()=>this.input};matches=()=>true;reportValidity=()=>this.valid;}
globalThis.HTMLFormElement=Form;globalThis.HTMLInputElement=Input;
try{
let submit;const paths=[];
bindPaginationJumps({addEventListener:(type,handler)=>{assert.equal(type,'submit');submit=handler;}},path=>paths.push(path));
const form=new Form();let prevented=0;
const send=()=>submit({target:form,preventDefault(){prevented++;}});
send();assert.equal(paths.at(-1),'/topics/sweet-love/page/3/?sort=2&source=menu');
form.input.valueAsNumber=1;send();assert.equal(paths.at(-1),'/topics/sweet-love/?sort=2&source=menu');
form.dataset.pageBase=searchPath('都市 / 爱情');form.input.valueAsNumber=2;send();assert.equal(paths.at(-1),searchPath('都市 / 爱情',2));
const count=paths.length;
for(const value of [NaN,0,-1,1.5,11,Infinity,Number.MAX_SAFE_INTEGER+1]){form.input.valueAsNumber=value;send();}
form.input.valueAsNumber=2;form.valid=false;send();assert.equal(paths.length,count);
assert.equal(prevented,11);
// The delegated listener works for forms added after it was registered.
const added=new Form();submit({target:added,preventDefault(){}});assert.equal(paths.length,count+1);
submit({target:{},preventDefault(){assert.fail('unrelated form');}});
}finally{
if(previousForm===undefined)delete globalThis.HTMLFormElement;else globalThis.HTMLFormElement=previousForm;
if(previousInput===undefined)delete globalThis.HTMLInputElement;else globalThis.HTMLInputElement=previousInput;
}
});
test('personal library and wallet requests and page links use the route page',async()=>{
const previousDocument=globalThis.document,calls=[];
const root={innerHTML:'',querySelector:()=>null,addEventListener(){}};
const user={userId:7,nickName:'测试用户'};
const api={user,ensureSession:async()=>user,async get(path,query){
calls.push({path,...query});
if(path==='user/center/data')return {};
if(path==='recharge/package/list')return {coinList:[],vipList:[]};
return {list:[],totalCount:120};
}};
const app={api,signal:new AbortController().signal};
globalThis.document={getElementById:()=>root};
try{
for(const [tab,endpoint] of [['favorites','playlet/collect/list'],['history','playlet/history/list'],['likes','playlet/like/list'],['purchased','playlet/bought/list']]){
await renderMember(app,tab,3,page=>paginationPath(`/member/${tab}/page/3/`,page));
const call=calls.at(-1);assert.equal(call.path,endpoint);assert.equal(call.pageNum,3);
assert.ok(root.innerHTML.includes(`href="/member/${tab}/page/2/"`));
}
await renderWallet(app,true,3,page=>paginationPath('/info/deposit/vip/page/3/',page));
assert.equal(calls.at(-1).path,'coin/trans/list');assert.equal(calls.at(-1).page,3);
assert.ok(root.innerHTML.includes('href="/info/deposit/vip/page/4/"'));
}finally{if(previousDocument===undefined)delete globalThis.document;else globalThis.document=previousDocument;}
});