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