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(path, transform=code=>code){ const source=await readFile(new URL(path,import.meta.url),'utf8'); const {outputText}=ts.transpileModule(source,{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}}); return 'data:text/javascript;base64,'+Buffer.from(transform(outputText).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))).toString('base64'); } const routingUrl=await moduleUrl('../src/lib/routing.ts'); const {parseSitePath,routeFromSegments,navigationSection,internalPagePath,getRankPage,RANK_PAGES,samePageAddress}=await import(routingUrl); const navigationUrl=await moduleUrl('../src/lib/client/navigation.ts',code=>code.replace(/from ['"]\.\.\/routing['"]/,`from '${routingUrl}'`)); const {navigateTo,NAVIGATE_EVENT}=await import(navigationUrl); const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts'); const renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace(/from ['"]\.\.\/api\/protocol['"]/,`from '${protocolUrl}'`)); const {card,hero,topicCard,renderHome}=await import(renderUrl); test('homepage URLs resolve to home independently of the shorts page',()=>{ for(const address of ['https://drama.example','https://drama.example/']){ const route=parseSitePath(new URL(address).pathname); assert.deepEqual(route,{group:'',key:''}); assert.equal(navigationSection(route),'home'); } assert.equal(internalPagePath('/','https://drama.example/shorts/'),'/'); assert.deepEqual(parseSitePath('/shorts/'),{group:'page',key:'shorts'}); }); test('clean paths select the matching page, IDs, and navigation section',()=>{ assert.deepEqual(parseSitePath('/'),{group:'',key:''}); assert.deepEqual(parseSitePath('/shorts/'),{group:'page',key:'shorts'}); assert.deepEqual(parseSitePath('/topics/31/'),{group:'page',key:'topics',value:'31'}); assert.deepEqual(parseSitePath('/play/23456/90001/'),{group:'play',key:'23456',value:'90001'}); assert.deepEqual(parseSitePath('/info/deposit/vip/'),{group:'info',key:'deposit',value:'vip'}); assert.deepEqual(parseSitePath('/member/history/'),{group:'member',key:'history'}); assert.deepEqual(parseSitePath('/member/likes/'),{group:'member',key:'likes'}); assert.deepEqual(parseSitePath('/member/likes/page/2/'),{group:'member',key:'likes',page:2}); for(const [path,section] of [['/','home'],['/shorts/','shorts'],['/topics/31/','topics'],['/detail/7/','shorts'],['/member/center/','']]){ assert.equal(navigationSection(parseSitePath(path)),section,path); } }); test('search keywords containing Chinese and URL punctuation survive a single decode',()=>{ const keyword='都市 / 爱情?#%2F'; const route={group:'search',key:keyword}; assert.deepEqual(parseSitePath('/search/'+encodeURIComponent(keyword)+'/'),route); assert.deepEqual(routeFromSegments(['search',keyword]),route); assert.equal(parseSitePath('/search/%E0%A4%A/'),null); assert.equal(navigationSection(route),''); assert.deepEqual(parseSitePath('/search/'),{group:'search',key:''}); assert.deepEqual(parseSitePath('/search'),{group:'search',key:''}); assert.deepEqual(parseSitePath('/search/page/2/','?q='+encodeURIComponent(keyword)),{...route,page:2}); }); test('resource IDs must be positive safe integers before any API reads',()=>{ for(const value of ['0','9007199254740992']){ for(const path of ['/detail/'+value+'/','/play/23456/'+value+'/','/topics/'+value+'/','/theme/'+value+'/','/tag/'+value+'/','/config/'+value+'/'])assert.equal(parseSitePath(path),null,path); } }); test('each ranking has a distinct page path and the correct API type',()=>{ for(const page of RANK_PAGES){ for(const path of [page.path,page.path.slice(0,-1)]){ const route=parseSitePath(path); assert.deepEqual(route,{group:'page',key:'rank',value:page.slug}); assert.equal(navigationSection(route),'rank'); assert.equal(getRankPage(route.value).rankType,page.rankType); assert.equal(internalPagePath(path,'https://drama.example/'),path); } } assert.deepEqual(RANK_PAGES.map(page=>page.rankType),[1,2,3]); assert.equal(getRankPage(parseSitePath('/rank/').value).rankType,1); for(const path of ['/rank/unknown/','/rank/new/extra/','/rank//','/rank/2/'])assert.equal(parseSitePath(path),null,path); }); test('hash page links are not converted or treated as page destinations',()=>{ const base='https://drama.example/'; for(const hash of ['#','#page/shorts','#page/config/31','#detail/7','#video/8','#play/7/90001','#search/'+encodeURIComponent('逆袭/甜宠'),'#member/history','#info/deposit/vip']){ assert.equal(internalPagePath(hash,base),null,hash); assert.equal(internalPagePath(base+hash,base),null,hash); } assert.deepEqual(parseSitePath(new URL(base+'#page/shorts').pathname),{group:'',key:''}); }); test('only recognized internal page destinations are handled by the application',()=>{ const base='https://drama.example/detail/7'; assert.equal(internalPagePath('/shorts/',base),'/shorts/'); assert.equal(internalPagePath('https://drama.example/member/history/',base),'/member/history/'); assert.equal(internalPagePath('/search/'+encodeURIComponent('爱情 / 逆袭')+'/',base),'/search/'+encodeURIComponent('爱情 / 逆袭')+'/'); for(const href of ['https://external.example/shorts','javascript:alert(1)','/api/app/playlet/home','/assets/poster.webp','#comments','/unknown','/play/7/','/shorts/extra/','/member/unknown/','/detail/0/']){ assert.equal(internalPagePath(href,base),null,href); } }); test('page navigation requests a complete server document while leaving hashes, external links and browser history to the browser',()=>{ const previous=globalThis.window,calls=[]; globalThis.window={location:{href:'https://drama.example/shorts/',assign:path=>calls.push(path)},history:{pushState:()=>assert.fail('client page routing must not replace server navigation')}}; try{ for(const path of ['#comments','/#page/shorts','https://external.example/shorts','/series/'])navigateTo(path); assert.deepEqual(calls,[]); for(const path of ['/play/23456/90001/','/member/history/','/rank/new/','/shorts/page/2/?sort=2'])navigateTo(path); assert.deepEqual(calls,['/play/23456/90001/','/member/history/','/rank/new/','/shorts/page/2/?sort=2']); assert.equal(globalThis.window.location.href,'https://drama.example/shorts/','the old document stays visible until the new response arrives'); }finally{globalThis.window=previous;} }); test('dynamic content generates trailing slashes directly in page links',()=>{ const drama={id:23456,title:'测试短剧',showOnBanner:true}; const topic={id:31,name:'测试专题'}; const cases=[ [card(drama),'/detail/23456/'], [hero([drama]),'/detail/23456/'], [topicCard(topic),'/topics/31/'], [topicCard(topic,true),'/topics/31/'], [renderHome({homeConfigData:[{id:31,name:'更多短剧',status:true,type:1,list:[drama]}]}),'/config/31/'], ]; for(const [html,path] of cases)assert.ok(html.includes(`href="${path}"`),path); assert.equal(internalPagePath('/search/'+encodeURIComponent('都市 / 爱情?#%')+'/?page=2','https://drama.example/'),'/search/'+encodeURIComponent('都市 / 爱情?#%')+'/?page=2'); }); test('server bootstrap matches only the same parsed page, pagination and query',()=>{ assert.equal(samePageAddress('/shorts/?sort=2&source=test','/shorts?source=test&sort=2'),true); assert.equal(samePageAddress('/search/%E9%83%BD%E5%B8%82/','/search/都市/'),true); assert.equal(samePageAddress('/shorts/page/2/','/shorts/page/3/'),false); assert.equal(samePageAddress('/shorts/?sort=2','/shorts/?sort=1'),false); assert.equal(samePageAddress('/unknown/','/unknown/'),false); }); test('rich text titles are one decoded segment, including the legacy richetext spelling',()=>{ const title='关于我们'; const route={group:'richtext',key:title}; for(const path of [`/richtext/${encodeURIComponent(title)}/`,`/richtext/${title}`,`/richetext/${encodeURIComponent(title)}/`]){ assert.deepEqual(parseSitePath(path),route,path); assert.equal(navigationSection(parseSitePath(path)),''); } assert.equal(internalPagePath(`/richtext/${encodeURIComponent(title)}/`,'https://drama.example/'),`/richtext/${encodeURIComponent(title)}/`); assert.equal(parseSitePath(`/richtext/${encodeURIComponent(title)}/page/2/`),null); for(const path of ['/richtext/','/richtext','/richetext/','/richtext/./','/richtext/../','/richtext/a/b/'])assert.equal(parseSitePath(path),null,path); }); test('removed standalone series and video paths are never recognized or converted to another page',()=>{ for(const path of ['/series','/series/','/series/page/2','/series/page/2/','/video/7','/video/7/','/video/7/page/2/']){ assert.equal(parseSitePath(path),null,path);assert.equal(internalPagePath(path,'https://drama.example/'),null,path); } assert.deepEqual(parseSitePath('/detail/7/'),{group:'detail',key:'7'}); assert.deepEqual(parseSitePath('/play/7/90001/'),{group:'play',key:'7',value:'90001'}); });