Files
web_hgdjc_h5/tests/rank-pages.test.mjs
T
谢宇宁andClaude Opus 5.5 f65d10e998 Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:07:59 +07:00

187 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {routingModuleUrl} from './helpers/routing.mjs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import vm from 'node:vm';
import ts from 'typescript';
import {seo,clientSeo,headDocument} from './helpers/seo.mjs';
const routingFile=new URL('../src/lib/routing.ts',import.meta.url);
const routingCode=ts.transpileModule(await readFile(routingFile,'utf8'),{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const routing=await import('data:text/javascript;base64,'+Buffer.from(routingCode+'\n//# sourceURL='+routingFile.pathname).toString('base64'));
const protocolFile=new URL('../src/lib/api/protocol.ts',import.meta.url);
const protocolCode=ts.transpileModule(await readFile(protocolFile,'utf8'),{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const protocolUrl='data:text/javascript;base64,'+Buffer.from(protocolCode).toString('base64');
const protocol=await import(protocolUrl);
const renderCode=ts.transpileModule(await readFile(new URL('../src/lib/client/render.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const renderUrl='data:text/javascript;base64,'+Buffer.from(renderCode.replace("'../routing'",JSON.stringify(routingModuleUrl)).replace('"../api/protocol"',JSON.stringify(protocolUrl))).toString('base64');
const searchCode=ts.transpileModule(await readFile(new URL('../src/lib/client/search.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const search=await import('data:text/javascript;base64,'+Buffer.from(searchCode.replace("'../routing'",JSON.stringify(routingModuleUrl)).replace("'../api/protocol'",JSON.stringify(protocolUrl)).replace("'./render'",JSON.stringify(renderUrl))).toString('base64'));
const infoCode=ts.transpileModule(await readFile(new URL('../src/lib/info-pages.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const infoPages=await import('data:text/javascript;base64,'+Buffer.from(infoCode).toString('base64'));
const siteNavigationCode=ts.transpileModule(await readFile(new URL('../src/lib/site-navigation.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
const applicationFile=new URL('../src/lib/client/application.ts',import.meta.url);
const applicationCode=ts.transpileModule(await readFile(applicationFile,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
const channelCode=ts.transpileModule(await readFile(new URL('../src/lib/client/channel.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
function fixture(load=async()=>({list:[{id:1}],totalCount:72,name:'接口通用标题'}),options={}){
const nodes=new Map(),calls=[],cardCalls=[],reloads=[];
const location={pathname:'/rank/hot/',search:'',origin:'https://site.test',reload:()=>reloads.push(location.pathname+location.search)};
const sidebar={dataset:{page:'rank'},classList:{toggle(name,active){this[name]=active;}},setAttribute(){},removeAttribute(){}};
const document={...headDocument(),querySelector(selector){
if(selector==='dialog[open]')return null;
if(!nodes.has(selector))nodes.set(selector,{innerHTML:'',textContent:'',hidden:false,dataset:{},setAttribute(name,value){this[name]=value;}});
return nodes.get(selector);
},querySelectorAll:selector=>selector.startsWith('a[data-page]')?[sidebar]:selector==='[data-promotion-row]'?options.promotionRows||[]:[]};
class ApiClient {async get(path,query,signal){calls.push({path,...query,signal});return load(path,query,signal);}async revalidateSession(){calls.push({path:'restore-session'});}}
const exports={},channelExports={},siteNavigationExports={};
const require=specifier=>{
if(specifier==='../site-navigation')return siteNavigationExports;
if(specifier==='./routing')return routing;
if(specifier==='./api/protocol')return protocol;
if(specifier==='./channel')return channelExports;
if(specifier==='./api')return {ApiClient};
if(specifier==='../routing')return routing;
if(specifier==='../api/protocol')return protocol;
if(specifier==='./search')return search;
if(specifier==='../info-pages')return infoPages;
if(specifier==='../seo')return seo;
if(specifier==='./seo')return {applyPageHead:head=>clientSeo.applyPageHead(head,document)};
if(specifier==='./config')return {configureSite:async()=>{}};
if(specifier==='./hero-swipe')return {bindHeroSwipe(){}};
if(specifier==='./promo-marquee')return {bindPromotionMarquee:root=>{calls.push({path:'bindPromotionMarquee',root});return ()=>calls.push({path:'disposePromotionMarquee',root});}};
if(specifier==='./account')return {renderMember:async(...args)=>calls.push({path:'renderMember',args}),renderWallet:async(...args)=>calls.push({path:'renderWallet',args})};
if(specifier==='./feedback')return {renderFeedbackPage:(...args)=>calls.push({path:'renderFeedbackPage',args})};
if(specifier==='./player')return {renderPlayer:async(_app,id,chapterId,isVideo,options)=>{calls.push({path:'renderPlayer',id,chapterId,isVideo,...options});return ()=>{};}};
if(specifier==='./render')return {escape:String,empty:message=>`<p>${message}</p>`,loading:()=>'<p>加载中</p>',activeBanners:()=>[],topicCard:()=>'<a href="/topics/1/">专题</a>',renderHome:()=>'<div data-home-loaded>接口首页</div>',card:(item,video,options)=>{cardCalls.push({item,video,options});return '<a href="/detail/1/">剧集</a>';},pagination:(_,page)=>`<span>第 ${page} 页</span>`};
return {};
};
vm.runInNewContext(siteNavigationCode,{exports:siteNavigationExports,require,URL,URLSearchParams});
vm.runInNewContext(channelCode,{exports:channelExports,require,URL,URLSearchParams});
vm.runInNewContext(applicationCode,{exports,require,document,location,AbortController,Event,Error,URL,URLSearchParams,clearInterval(){},window:{location,scrollTo(){},dispatchEvent(){}}},{filename:applicationFile.pathname});
return {app:new exports.Application({},true,options.seo,options.page,undefined,options.shared),nodes,calls,cardCalls,location,document,sidebar,reloads,channel:channelExports};
}
test('all native pages bind every square ad row and clean up old loops before rebinding',async()=>{
for(const pathname of ['/','/shorts/','/detail/7/','/play/7/8/','/member/favorites/','/info/deposit/']){
const rows=[{name:'first row'},{name:'second row'}],f=fixture(undefined,{promotionRows:rows});
f.location.pathname=pathname;await f.app.route();
assert.deepEqual(f.calls.filter(call=>call.path==='bindPromotionMarquee').map(call=>call.root),rows,pathname);
f.app.bindPromotions();
assert.deepEqual(f.calls.filter(call=>call.path==='disposePromotionMarquee').map(call=>call.root),rows,pathname);
assert.equal(f.calls.filter(call=>call.path==='bindPromotionMarquee').length,4,pathname);
}
});
test('about uses business Telegram and the feedback group while retaining configured service hours',()=>{
const page=infoPages.getInfoPage('about-us',{config:{telegram:'https://t.me/business_team',tgGroup:'https://t.me/feedback_group',gmailAddr:'ignored@example.com',customerServiceHost:'https://ignored.example.com'}},[
{name:'商务合作',value:'business@example.com'},{name:'用户反馈',value:'mailto:support@example.com'},
{name:'服务时间',value:'周一至周五 09:00–18:00'},{name:'邮箱',value:'ignored@example.com'},
]);
assert.ok(page.html.includes('商务合作:<a href="https://t.me/business_team"'));
assert.ok(page.html.includes('用户反馈:<a href="https://t.me/feedback_group"'));
assert.ok(page.html.includes('<p>服务时间:周一至周五 09:00–18:00</p>'));
assert.ok(!page.html.includes('mailto:'));assert.ok(!page.html.includes('@example.com'));assert.ok(!page.html.includes('ignored.example.com'));
});
test('Telegram contacts prefer site configuration, fall back to address configuration and hide absent channels',()=>{
const html=infoPages.infoContactSection({config:{telegram:'',tgGroup:''},newAddressInfo:{telegram:'@business_team',tgGroup:'https://t.me/feedback_group'}},null);
assert.ok(html.includes('商务合作:<a href="https://t.me/business_team"'));assert.ok(html.includes('用户反馈:<a href="https://t.me/feedback_group"'));
assert.ok(!html.includes('服务时间'));
const preferred=infoPages.infoContactSection({config:{telegram:'https://t.me/primary'},newAddressInfo:{telegram:'https://t.me/fallback'}});
assert.ok(preferred.includes('t.me/primary'));assert.ok(!preferred.includes('t.me/fallback'));assert.ok(!preferred.includes('用户反馈'));
assert.ok(infoPages.infoContactSection({config:{gmailAddr:'help@example.com',customerServiceHost:'https://ignored.example.com'}}).includes(' hidden'));
assert.ok(infoPages.infoContactSection({config:{telegramAdd:'https://t.me/legacy'}}).includes('t.me/legacy'));
});
test('configured contacts escape text and reject executable or malformed addresses',()=>{
const html=infoPages.infoContactSection({},[
{name:'<img onerror=alert(1)>',value:'<script>alert(1)</script>'},
{name:'注入',value:'javascript:alert(1)'},
{name:'数据链接',value:'data:text/html,<script>alert(1)</script>'},
{name:'正常链接',value:'https://example.com/?a=1&b=2'},null,
]);
assert.ok(html.includes('&lt;script&gt;'));assert.ok(html.includes('&lt;img onerror=alert(1)&gt;'));
assert.ok(!html.includes('<script'));assert.ok(!html.includes('javascript:'));assert.ok(!html.includes('data:text'));
assert.ok(html.includes('href="https://example.com/?a=1&amp;b=2" target="_blank" rel="sponsored nofollow noopener noreferrer"'));
assert.ok(infoPages.infoContactSection({config:{gmailAddr:'javascript:bad@example.com'}}).includes(' hidden'));
});
test('ranking data and native page links derive from the server route and preserve global positions',()=>{
const f=fixture();
for(const item of routing.RANK_PAGES){
const route=routing.parseSitePath(item.path+'page/2/'),request=f.channel.channelRequest(route);
assert.equal(request.endpoint,'playlet/hot/rank/list');assert.equal(request.query.rankType,item.rankType);assert.equal(request.query.pageNum,2);
const view=f.channel.channelView(route,{list:[{id:1}],totalCount:72},item.path+'page/2/');
assert.equal(view.title,item.title);assert.ok(view.html.includes('href="'+item.path+'" aria-current="page"'));assert.equal(f.cardCalls.at(-1).options.rank,25);
}
});
test('fixed information definitions remain static and contain no executable content',()=>{
for(const page of Object.values(infoPages.INFO_PAGES)){assert.ok(page.html);assert.ok(!page.html.includes('<script'));assert.ok(!page.html.includes('href="#'));}
});
test('initial server channel and search content is retained without page requests or loading placeholders',async()=>{
for(const page of [{path:'/shorts/page/2/?sort=2',panel:'channel',title:'服务端栏目'},{path:'/search/'+encodeURIComponent('都市 爱情')+'/',panel:'search',title:'搜索结果',search:{keyword:'都市 爱情',total:40,page:1,pageSize:25}}]){
const f=fixture(()=>assert.fail('page data belongs to SSR'),{page});
const address=new URL(page.path,'https://site.test');f.location.pathname=address.pathname;f.location.search=address.search;
const selector=page.panel==='channel'?'#channel-content':'#search-results-page';f.document.querySelector(selector).innerHTML='<article>完整首屏正文</article>';
await f.app.route();assert.equal(f.nodes.get(selector).innerHTML,'<article>完整首屏正文</article>');assert.equal(f.calls.length,0);
await f.app.route();assert.deepEqual(f.reloads,[page.path]);assert.equal(f.nodes.get(selector).innerHTML,'<article>完整首屏正文</article>');
}
});
test('server player data binds once with its comment route and never refetches the initial content',async()=>{
const player={detail:{id:23456,title:'剧集'},chapters:[],chapter:null,comments:{list:[],totalCount:0}};
const path='/play/23456/90001/page/2/',f=fixture(()=>assert.fail('no page fetch'),{page:{path:path+'?commentSort=1',panel:'player',title:'剧集',player}});
f.location.pathname=path;f.location.search='?commentSort=1';await f.app.route();
assert.equal(f.calls.length,1);assert.equal(f.calls[0].path,'renderPlayer');assert.equal(f.calls[0].initial,player);assert.equal(f.calls[0].preserveMarkup,true);assert.equal(f.calls[0].page,2);assert.equal(f.calls[0].commentSort,1);
assert.equal(f.calls[0].pageHref(3,1),'/play/23456/90001/page/3/?commentSort=1');await f.app.route();assert.equal(f.calls.length,1);assert.equal(f.reloads.length,1);
});
test('private and feedback pages only bind interactions to their prepared snapshots',async()=>{
for(const [path,info,handler] of [['/member/settings/',{key:'settings',member:{user:{userId:7}}},'renderMember'],['/info/deposit/',{key:'deposit',wallet:{user:{userId:7},packages:{coinList:[]},transactions:{list:[]}}},'renderWallet'],['/info/feedback/',{key:'feedback',feedback:true},'renderFeedbackPage']]){
const f=fixture(()=>assert.fail('no page fetch'),{page:{path,panel:'info',title:'完整页面',info}});f.location.pathname=path;await f.app.route();
assert.equal(f.calls.length,1);assert.equal(f.calls[0].path,handler);assert.equal(f.calls[0].args.at(-1),true);
}
});
test('cold unavailable member and wallet pages client-render after guest bootstrap',async()=>{
for(const [path,handler,tab] of [['/member/center/','renderMember','center'],['/member/history/','renderMember','history'],['/info/deposit/','renderWallet',undefined],['/info/deposit/vip/','renderWallet',undefined],['/member/settings/','renderMember','settings']]){
const f=fixture(()=>assert.fail('no page fetch'),{page:{path,panel:'info',title:'完整页面',info:{key:path.includes('deposit')?'deposit':tab},session:{user:null,state:'unavailable'}}});
f.location.pathname=path;await f.app.route();
assert.equal(f.calls.length,1,path);assert.equal(f.calls[0].path,handler,path);
if(handler==='renderMember')assert.equal(f.calls[0].args[1],tab);
if(path==='/info/deposit/vip/')assert.equal(f.calls[0].args[1],true);
if(path==='/info/deposit/')assert.equal(f.calls[0].args[1],false);
}
const signedOut=fixture(()=>assert.fail('login-required must keep SSR'),{page:{path:'/member/center/',panel:'info',title:'完整页面',info:{key:'center'},session:{user:null,state:'login-required'}}});
signedOut.location.pathname='/member/center/';await signedOut.app.route();assert.equal(signedOut.calls.length,0);
});
test('missing or mismatched snapshots leave server error HTML visible without a client retry',async()=>{
const f=fixture(()=>assert.fail('errors must not trigger a second page request'),{page:{path:'/shorts/',panel:'channel',title:'旧页'}});
f.location.pathname='/topics/7/';f.document.querySelector('#channel-content').innerHTML='<p>服务暂不可用</p>';await f.app.route();assert.equal(f.calls.length,0);assert.equal(f.nodes.get('#channel-content').innerHTML,'<p>服务暂不可用</p>');
});
test('client configuration preserves the server content metadata and canonical',async()=>{
const path='/topics/7/',environment=seo.resolveSeoEnvironment({config:{foreverUrlAddr:'https://canonical.drama.test'}}),content={title:'服务端专题',description:'服务端简介',totalCount:72,pageSize:24};
const f=fixture(()=>assert.fail('no page fetch'),{page:{path,panel:'channel',title:'服务端专题'},seo:{environment,content,path}});f.location.pathname=path;await f.app.route();
await f.app.applyConfig({config:{foreverUrlAddr:'https://other.drama.test'}});
assert.equal(f.document.title,'服务端专题 - 禁果短剧');assert.equal(f.document.value('link[rel="canonical"]'),'https://canonical.drama.test/topics/7/');
});
test('shared server configuration initializes downloads and contacts without refetching page configuration or footer',async()=>{
const shared={config:{config:{officialAppAddr:'https://app.test/download'}},footer:{footerInfo:{contactList:[{name:'服务时间',value:'每天'}]}},trackerConfigured:false};
const f=fixture(()=>assert.fail('public settings already arrived in HTML'),{shared});
await f.app.initializeShared();assert.equal(f.app.config.config.officialAppAddr,'https://app.test/download');assert.equal(f.calls.length,0);
});
test('public back/forward documents keep working without reload; interactive private pages revalidate as fresh documents',async()=>{
const f=fixture();f.location.pathname='/shorts/';f.app.hidePage(true);assert.equal(f.app.signal.aborted,false);
f.app.restorePage();assert.equal(f.reloads.length,0);assert.deepEqual(f.calls.map(item=>item.path),['restore-session']);
const privatePage=fixture();privatePage.location.pathname='/info/deposit/';privatePage.app.hidePage(true);assert.equal(privatePage.app.signal.aborted,true);
privatePage.app.restorePage();assert.equal(privatePage.reloads.length,1);
});