Files
2026-11-06 00:13:27 +07:00

151 lines
9.1 KiB
JavaScript

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(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(linkShared(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/about-us/'),{group:'info',key:'about-us'});
for(const path of ['/info/deposit/vip/','/info/deposit/','/info/feedback/','/info/user-center/','/info/settings/','/member/history/','/member/likes/','/member/likes/page/2/','/member/center/'])assert.equal(parseSitePath(path),null,path);
for(const [path,section] of [['/','home'],['/shorts/','shorts'],['/topics/31/','topics'],['/detail/7/','shorts']]){
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/info/about-us/',base),'/info/about-us/');
assert.equal(internalPagePath('https://drama.example/member/history/',base),null);
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/','/info/about-us/','/rank/new/','/shorts/page/2/?sort=2'])navigateTo(path);
assert.deepEqual(calls,['/play/23456/90001/','/info/about-us/','/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'});
});