Files
web_hgdjc_h5/tests/slug-routing.test.mjs
2026-11-06 00:13:27 +07:00

103 lines
7.7 KiB
JavaScript

import {linkShared} from './helpers/shared-modules.mjs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import ts from 'typescript';
import {routingModuleUrl} from './helpers/routing.mjs';
import {seo} from './helpers/seo.mjs';
const routes=await import(routingModuleUrl);
async function moduleUrl(path,imports={}){
const {outputText}=ts.transpileModule(await readFile(new URL('../src/lib/'+path,import.meta.url),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}});
let code=outputText;
for(const [key,url] of Object.entries({'../routing':routingModuleUrl,...imports}))for(const quote of ["'",'"'])code=code.replace(quote+key+quote,JSON.stringify(url));
return 'data:text/javascript;base64,'+Buffer.from(linkShared(code)).toString('base64');
}
const protocol=await moduleUrl('api/protocol.ts');
const renderUrl=await moduleUrl('client/render.ts',{'../api/protocol':protocol});
const render=await import(renderUrl);
const channel=await import(await moduleUrl('client/channel.ts',{'../api/protocol':protocol,'./render':renderUrl}));
const search=await import(await moduleUrl('client/search.ts',{'../api/protocol':protocol,'./render':renderUrl}));
const drama={id:9,slug:'city-story',title:'都市故事',desc:'作品介绍',cover:'https://media.drama.test/poster.webp',onlineAt:'2026-10-20'};
const environment=seo.seoEnvironment({config:{foreverUrlAddr:'https://drama.test'}});
test('slug-first paths encode one segment once, preserve ID fallback and accept direct paginated requests',()=>{
for(const key of ['city-story','city_story-2026','dushiaiqing']){
for(const kind of ['detail','topics','theme','tag','config']){
const path=routes.contentPath(kind,{id:9,slug:' '+key+' '});
assert.equal(path,`/${kind}/${encodeURIComponent(key)}/`);
assert.equal(routes.parseSitePath(path)[kind==='detail'?'key':'value'],key);
if(kind==='detail'){assert.equal(routes.parseSitePath(path+'page/2/'),null);continue;}
assert.equal(routes.paginationPath(path,2),path+'page/2/');
assert.equal(routes.parseSitePath(path+'page/2/').page,2);
}
assert.deepEqual(routes.parseSitePath(routes.contentPath('play',{id:9,slug:key},90)),{group:'play',key,value:'90'});
}
for(const slug of [undefined,null,'',' ','..','bad\\path','都市爱情','city-爱情','都市 / 爱情?#%2F','%E9%83%BD%E5%B8%82'])assert.equal(routes.contentPath('detail',{id:9,slug}),'/detail/9/');
assert.deepEqual(routes.contentQuery('city-story','playletId','playletSlug'),{playletSlug:'city-story'});
assert.deepEqual(routes.contentQuery('009'),{id:9});
assert.equal(routes.parseSitePath('/play/city-story/chapter-slug/'),null);
});
test('Chinese upstream slugs use IDs in every generated link and SEO URL without changing API identity',()=>{
const item={...drama,slug:'都市爱情'};
assert.ok(render.card(item).includes('href="/detail/9/"'));
assert.ok(render.hero([item]).includes('href="/detail/9/"'));
assert.ok(render.topicCard({id:7,slug:'都市合集',name:'都市合集'}).includes('href="/topics/7/"'));
assert.ok(render.blockSection({id:31,slug:'必看好剧',name:'必看好剧',type:1,status:true,list:[item]}).includes('href="/config/31/"'));
const chapter={id:90,currentEpisode:3,canWatch:true,videoUrl:'https://media.drama.test/3.mp4'};
const head=seo.buildPageHead('/play/9/90/','',environment,seo.dramaSeoContent(item,chapter));
assert.equal(head.canonical,'https://drama.test/play/9/90/');
assert.ok(!JSON.stringify(head.jsonLd).includes('%E9%83%BD'));
assert.equal(routes.matchesContentKey(item,'9'),true);
assert.deepEqual(routes.contentQuery('9'),{id:9});
// Previously issued Chinese slug links remain readable, but their canonical uses the ID.
assert.equal(routes.matchesContentKey(item,'都市爱情'),true);
assert.equal(seo.buildPageHead('/detail/都市爱情/','',environment,seo.dramaSeoContent(item)).canonical,'https://drama.test/detail/9/');
});
test('cards, carousel, topics, more lists and suggestions prefer slug while explicit moreUrl remains authoritative',()=>{
for(const html of [render.card(drama),render.hero([drama]),search.searchSuggestionsMarkup([drama],'都市')])assert.ok(html.includes('href="/detail/city-story/"'));
assert.ok(render.topicCard({id:7,slug:'city-collection',name:'都市合集'}).includes('href="/topics/city-collection/"'));
const block={id:31,slug:'editors-choice',name:'编辑精选',type:1,status:true,list:[drama]};
assert.ok(render.blockSection(block).includes('href="/config/editors-choice/"'));
assert.ok(render.blockSection({...block,moreUrl:'/shorts/'}).includes('href="/shorts/"'));
assert.ok(render.card({...drama,slug:''}).includes('href="/detail/9/"'));
});
test('topic slugs query detail directly and retain the response identity',async()=>{
const calls=[];
const data=await channel.loadChannel({get:async(path,query)=>{calls.push({path,query});return {id:7,slug:'city-collection',name:'都市合集',list:[drama],totalCount:72};}},{group:'page',key:'topics',value:'city-collection',page:2});
assert.deepEqual(calls,[{path:'playlet/topic/detail',query:{pageNum:2,pageSize:24,topicSlug:'city-collection',dataType:1,sortType:1}}]);
assert.equal(data.id,7);assert.equal(data.slug,'city-collection');
const view=channel.channelView({group:'page',key:'topics',value:'7'},data,'/topics/7/?sort=1');
assert.ok(view.html.includes('href="/topics/city-collection/page/2/?sort=1"'));
const missing=await channel.loadChannel({get:async()=>({id:9,slug:'different',list:[]})},{group:'page',key:'topics',value:'city-collection'});
assert.equal(missing.missing,true);
await assert.rejects(channel.loadChannel({get:async()=>{throw Error('outage');}},{group:'page',key:'topics',value:'city-collection'}),/outage/);
});
test('tag slug query and paginated home-config response preserve numeric business identity',async()=>{
assert.deepEqual(channel.channelRequest({group:'page',key:'tag',value:'urban',page:2}).query,{pageNum:2,pageSize:24,tagSlug:'urban',sortType:1});
const data=await channel.loadChannel({get:async()=>({config:{id:31,slug:'editors-choice',title:'编辑精选',desc:'精选介绍',moduleType:1},list:[drama],totalCount:30})},{group:'page',key:'config',value:'editors-choice'});
assert.equal(data.id,31);assert.equal(data.slug,'editors-choice');assert.equal(data.totalCount,30);
});
test('canonical, pagination, series/episode schema and homepage ItemList use the same slug paths',()=>{
const chapter={id:90,playletId:9,currentEpisode:3,canWatch:true,videoUrl:'https://media.drama.test/3.mp4'};
const content=seo.dramaSeoContent(drama,chapter);
for(const key of ['9','city-story']){
const head=seo.buildPageHead(`/play/${key}/90/`,'',environment,content);
assert.equal(head.canonical,'https://drama.test/play/city-story/90/');
const graph=head.jsonLd['@graph'];
assert.equal(graph.find(node=>node['@type']==='TVSeries').url,'https://drama.test/detail/city-story/');
assert.equal(graph.find(node=>node['@type']==='TVEpisode').url,head.canonical);
assert.equal(graph.find(node=>node['@type']==='BreadcrumbList').itemListElement[2].item,'https://drama.test/detail/city-story/');
}
const list=seo.listSeoContent({id:7,slug:'city-collection',list:[drama],totalCount:72},{group:'page',key:'theme',value:'7',page:2});
const head=seo.buildPageHead('/theme/7/page/2/','',environment,list);
assert.equal(head.canonical,'https://drama.test/theme/city-collection/page/2/');assert.equal(head.previous,'https://drama.test/theme/city-collection/');
const home=seo.buildPageHead('/','',environment,seo.homeSeoContent({homeBannerData:[drama]}));
assert.equal(home.jsonLd['@graph'].find(node=>node['@type']==='ItemList').itemListElement[0].url,'https://drama.test/detail/city-story/');
});