import {linkShared} from './helpers/shared-modules.mjs'; 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(linkShared(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')); 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)+'/']; 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/','/info/about-us/','/detail/23456/','/play/23456/90001/'])assert.equal(parseSitePath(base+'page/2/'),null,base); for(const base of ['/member/center/','/member/favorites/','/info/deposit/','/info/feedback/'])assert.equal(parseSitePath(base),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,'都市 / 爱情?#%'); }); 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; } });