Files
web_hgcg_h5/tests/rank-pages.test.mjs
2026-10-27 17:39:27 +07:00

141 lines
12 KiB
JavaScript
Raw Permalink 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.replace('"./site"',JSON.stringify('data:text/javascript;base64,'+Buffer.from(ts.transpileModule(await readFile(new URL('../src/lib/site.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText).toString('base64')));
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==='./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/','/info/about-us/']){
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 and DMCA show only the official TG contact, ignoring configured channels, footer contacts and email',()=>{
const about=infoPages.getInfoPage('about-us').html;
assert.ok(about.includes('官方TG:<a href="https://t.me/jbd125"'));assert.ok(about.includes('>@jbd125</a>'));
for(const text of ['商务合作','用户反馈','服务时间','mailto:','gmail'])assert.ok(!about.includes(text),text);
const dmca=infoPages.getInfoPage('dmca').html;
assert.ok(dmca.includes('href="https://t.me/jbd125"'));assert.ok(!dmca.includes('mailto:'));assert.ok(!dmca.includes('gmail'));
assert.equal(infoPages.infoContactSection(),infoPages.infoContactSection());
});
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 and never refetches the initial content',async()=>{
const player={detail:{id:23456,title:'剧集'},chapters:[],chapter:null};
const path='/play/23456/90001/',f=fixture(()=>assert.fail('no page fetch'),{page:{path,panel:'player',title:'剧集',player}});
f.location.pathname=path;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,undefined);assert.equal(f.calls[0].commentSort,undefined);
await f.app.route();assert.equal(f.calls.length,1);assert.equal(f.reloads.length,1);
});
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://huangguocg.com'}}),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://huangguocg.com/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:'每天'}]}}};
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 player 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='/play/7/8/';privatePage.app.hidePage(true);assert.equal(privatePage.app.signal.aborted,true);
privatePage.app.restorePage();assert.equal(privatePage.reloads.length,1);
});