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

283 lines
20 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 {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))+'\n//# sourceURL='+new URL(path,import.meta.url).pathname).toString('base64');
}
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 {ads,promotionRow,renderHome,activeBanners,hero,homeRecommendations,heroFeed,card,coverCornerBadge,topicCard,imageAttributes,blockSection}=await import(renderUrl);
const drama={id:23456,title:'正常剧集 <script>alert(1)</script>',cover:'https://example.com/poster.webp',tagNames:['都市']};
const advert={id:'ad-one',title:'有效广告',images:'https://example.com/ad.webp',link:'https://example.com/landing'};
const itemCount=html=>(html.match(/class="(?:hg-card|curated-card drama-data-card|ranked-card drama-data-card|topic-recommendation-card)"/g)||[]).length;
test('Swagger homepage modules render actual titles, drama cards and theme destinations',()=>{
const html=renderHome({modules:[{id:31,title:'新接口短剧栏目',slug:'editors',moduleType:1,list:[drama]},{id:32,title:'新接口题材',moduleType:2,topicList:[{id:7,slug:'urban',name:'都市题材',list:[drama]}]}]});
for(const text of ['新接口短剧栏目','新接口题材','href="/config/editors/"','href="/theme/urban/"','href="/detail/23456/"'])assert.ok(html.includes(text),text);
assert.equal(homeRecommendations({modules:[{id:31,title:'短剧',moduleType:1,list:[drama]}]})[0].id,drama.id);
const emptyModules=renderHome({modules:[],homeConfigData:[{id:1,name:'旧缓存不应复活',type:1,status:true,list:[drama]}]});
assert.ok(!emptyModules.includes('旧缓存不应复活'));
});
test('topic cards escape metadata and carry no analytics attributes',()=>{
const html=topicCard({id:7,name:'都市 <甜宠>',icon:'https://example.com/topic.png'});
assert.ok(html.includes('都市 &lt;甜宠&gt;'));assert.ok(!html.includes('data-track'));assert.ok(!html.includes('data-item-'));
});
test('drama cards use the 9:16 gallery layout with episode counter, square badge and views/theme meta',()=>{
const series=card({id:5,title:'连载剧',totalEpisode:12,updateStatus:1,isFeatured:true,heatValue:12345,themeNames:['都市'],cover:'https://example.com/c.webp'});
for(const text of ['class="hg-card"','class="hg-card-cover"','>1/12+<','class="hg-card-badge"','cover-corner-badge--hot','class="hg-card-rule"','class="hg-card-views"','1.2万','class="hg-card-theme">都市<'])assert.ok(series.includes(text),text);
assert.ok(!series.includes('drama-description'));assert.ok(!series.includes('data-track'));
assert.ok(card({id:6,title:'完结',totalEpisode:30,updateStatus:2}).includes('>1/30<'));
assert.ok(card({id:7,title:'单集',totalEpisode:1}).includes('>单集<'));
});
test('API images use data sources and local placeholders until decrypted, including video posters',()=>{
const cases=[card(drama),hero([drama]),topicCard({id:1,icon:drama.cover}),ads([advert])];
for(const html of cases){
const images=[...html.matchAll(/<img\b[^>]*>/g)];assert.ok(images.length>0);
for(const [tag] of images){
assert.match(tag,/\ssrc="\/placeholder\.png"/);assert.match(tag,/\sdata-src="https:\/\//);
assert.ok(!/\ssrc="https?:/.test(tag));
}
}
assert.match(imageAttributes(drama.cover,undefined,'poster'),/^poster="\/placeholder\.png" data-poster="https:\/\//);
assert.equal(imageAttributes('javascript:alert(1)','/icon.svg'),'src="/icon.svg"');
assert.ok(imageAttributes('https://example.com/a?one=1&two=2').includes('one=1&amp;two=2'));
});
test('ads tolerates null, missing, and non-array API fields',()=>{
for(const value of [null,undefined,{},'',false,0,'[]'])assert.equal(ads(value),'');
assert.equal(ads([]),'');
});
test('shared square ad rows keep real ad data and links and omit empty placements',()=>{
const html=promotionRow([advert,{...advert,id:'ad-two',title:'第二个广告'}]);
assert.ok(html.includes('class="promo-marquee" data-promotion-row'));
assert.equal((html.match(/class="promo-tile"/g)||[]).length,2);
assert.ok(html.includes('alt="第二个广告"'));assert.ok(!html.includes('data-ad-'));
assert.ok(html.includes('href="https://example.com/landing"'));
assert.ok(html.includes('width="132" height="132"'));assert.ok(html.includes('alt="有效广告"'));
for(const value of [null,{},[],[null]])assert.equal(promotionRow(value),'');
});
test('ads ignores malformed entries and keeps valid ads in order with escaped text',()=>{
const html=ads([null,undefined,false,42,[],{...advert,images:null},{...advert,images:'javascript:alert(1)'},advert,{...advert,id:'two',title:'第二条 <script>',link:'javascript:alert(1)'}]);
assert.equal((html.match(/class="promo-app"/g)||[]).length,2);
assert.ok(html.indexOf('有效广告')<html.indexOf('第二条'));
assert.ok(html.includes('href="https://example.com/landing"'));
assert.ok(!html.includes('/apps/'));assert.ok(html.includes('<a class="promo-app"><img'));assert.ok(html.includes('第二条 &lt;script&gt;'));
assert.ok(!html.includes('javascript:'));assert.ok(!html.includes('<script>'));
});
test('home renders when nineGridList is explicitly null, preserving valid content',()=>{
const html=renderHome({homeBannerData:[drama],homeConfigData:[{id:31,name:'后台栏目',type:1,status:true,list:[drama]}],advitise:{nineGridList:null}});
assert.ok(html.includes('后台栏目'));assert.ok(html.includes('href="/detail/23456/"'));
assert.ok(html.includes('正常剧集 &lt;script&gt;'));assert.ok(!html.includes('<script>alert(1)</script>'));
});
test('dynamic home sections share a grid without inheriting the prototype editorial rows',()=>{
const html=renderHome({homeConfigData:[
{id:31,name:'必看好剧',status:true,type:1,list:[drama,{...drama,id:2,totalEpisode:10}]},
{id:32,name:'热播榜',status:true,type:1,list:[{...drama,id:3}]},
{id:33,name:'剧集',status:true,type:2,videoList:[{...drama,id:4,totalEpisode:8}]},
{id:34,name:'题材',status:true,type:3,themeList:[{id:8,name:'都市'}]},
]});
assert.equal((html.match(/class="topic-section home-block"/g)||[]).length,3);
assert.equal((html.match(/class="cards"/g)||[]).length,2);
assert.ok(!html.includes('id="romance-rows"'));assert.ok(!html.includes('romance-editorial'));
assert.ok(!html.includes('id="library"'));
for(const id of [31,32,34])assert.ok(html.includes(`href="/config/${id}/"`));
for(const id of [23456,2,3])assert.ok(html.includes(`href="/detail/${id}/"`));
assert.ok(!html.includes('href="/video/4/"'));assert.ok(!html.includes('href="/config/33/"'));assert.ok(html.includes('href="/topics/8/"'));
assert.equal((html.split('id="home-blocks"')[1].match(/class="hg-card-counter"/g)||[]).length,3);
assert.ok(html.indexOf('必看好剧')<html.indexOf('热播榜'));
});
test('homepage banner ads follow each second topic section and keep real links',()=>{
const banner=id=>({id,title:'横幅 '+id,images:'https://cdn.example/'+id+'.gif',link:'https://example.com/'+id});
const block=(id,name)=>({id,name,status:true,type:1,list:[drama]});
const html=renderHome({homeConfigData:[block(1,'栏目一'),block(2,'栏目二'),block(3,'栏目三'),block(4,'栏目四'),block(5,'栏目五')],advitise:{detailbannerList:[null,banner('a'),{...banner('b'),images:'javascript:alert(1)'},banner('c'),banner('d'),banner('e'),banner('f')]}});
const blocks=html.split('id="home-blocks"')[1].split('popular-searches')[0];
assert.equal((blocks.match(/class="wide-ad-row"/g)||[]).length,2);
assert.ok(blocks.indexOf('栏目二')<blocks.indexOf('横幅 a')&&blocks.indexOf('横幅 a')<blocks.indexOf('横幅 c')&&blocks.indexOf('横幅 c')<blocks.indexOf('栏目三'));
assert.ok(blocks.indexOf('栏目四')<blocks.indexOf('横幅 d')&&blocks.indexOf('横幅 d')<blocks.indexOf('横幅 e')&&blocks.indexOf('横幅 e')<blocks.indexOf('栏目五'));
assert.ok(!blocks.includes('横幅 f'));assert.ok(!blocks.includes('横幅 b'));assert.ok(!blocks.includes('javascript:'));
assert.ok(!blocks.includes('data-ad-'));assert.ok(blocks.includes('href="https://example.com/a"'));
assert.ok(!renderHome({homeConfigData:[block(1,'栏目一'),block(2,'栏目二')]}).includes('wide-ad-row'));
assert.ok(!renderHome({homeConfigData:[block(1,'单独栏目')],advitise:{detailbannerList:[banner('a')]}}).includes('wide-ad-row'));
});
test('empty or malformed homepage lists fall back to empty states instead of throwing',()=>{
for(const value of [null,undefined,{},'',false,1]){
const html=renderHome({homeBannerData:value,homeConfigData:value,announcements:value,advitise:{nineGridList:value}});
assert.ok(html.includes('暂无推荐剧集'));assert.ok(html.includes('暂时没有内容'));
}
assert.ok(renderHome(null).includes('暂无推荐剧集'));
});
test('homepage category styles follow API values rather than titles, ranks or module positions',()=>{
const modules=[
{id:1,slug:'first',title:'热门推荐',moduleType:1,rank:99,style:' d ',list:[{...drama,slug:'first-story'}]},
{id:2,title:'热门推荐',moduleType:1,rank:1,style:'C',label:'样式C <script>',list:[drama]},
{id:3,title:'随意命名',moduleType:1,style:'B',list:[drama]},
{id:4,title:'随意命名',moduleType:1,style:'A',list:[drama]},
{id:5,title:'随意命名',moduleType:2,style:'E',topicList:[{id:8,slug:'urban',name:'都市'}]},
];
const html=renderHome({modules});
assert.deepEqual([...html.matchAll(/data-home-style="([A-E])"/g)].map(match=>match[1]),['D','C','B','A','E']);
assert.ok(html.includes('class="topic-section romance-editorial home-block" data-home-style="A"'));
assert.ok(html.includes('id="library" class="home-block" data-home-style="B"'));
assert.ok(html.includes('class="topic-section home-block" data-home-style="C"'));
assert.ok(html.includes('class="topic-section home-block" data-home-style="D"'));
assert.ok(html.includes('class="topic-section topic-recommendations home-block" data-home-style="E"'));
assert.ok(html.includes('class="ranked-card drama-data-card"'));assert.ok(html.includes('class="curated-card drama-data-card"'));
assert.ok(html.includes('href="/detail/first-story/"'));assert.ok(html.includes('href="/config/first/"'));assert.ok(html.includes('href="/theme/urban/"'));
assert.ok(html.includes('class="pick">最新上架</span>'));assert.ok(!html.includes('样式C'));assert.ok(!html.includes('<script>'));
});
test('A–E previews use API limits while retaining backend order and full source lists',()=>{
for(const [style,limit] of [['A',3],['B',9],['C',8],['D',7],['E',5]]){
const list=Array.from({length:14},(_,index)=>({...drama,id:500-index,slug:'story-'+index,countLike:index}));
const topics=list.map(item=>({id:item.id,name:'专题 '+item.id,slug:item.slug}));
const block={id:1,name:'任意分类',type:style==='E'?3:1,status:true,style,limit,list,themeList:topics,topicKind:'theme'};
const html=blockSection(block);
assert.equal(itemCount(html),limit,style);
const links=[...html.matchAll(/href="\/(?:detail|theme)\/story-(\d+)\/"/g)].map(match=>Number(match[1]));
assert.deepEqual(links,Array.from({length:limit},(_,index)=>index),style);
assert.equal(list.length,14);assert.equal(topics.length,14);
if(style==='C'){
assert.equal((html.match(/class="curated-card drama-data-card"/g)||[]).length,limit);
assert.equal((html.match(/class="watch-link"/g)||[]).length,limit);
}
if(style==='D'){
for(let rank=1;rank<=limit;rank++)assert.ok(html.includes(`aria-label="第 ${rank} 名,查看`));
assert.ok(html.includes('>01</strong>'));assert.ok(html.includes('>07</strong>'));assert.ok(!html.includes('rank-home-badge'));
assert.ok(blockSection({...block,id:2,list:[drama]}).includes('aria-label="第 1 名,查看'));
}
}
});
test('category limits apply to current and legacy modules, including unknown styles',()=>{
const list=Array.from({length:14},(_,index)=>({...drama,id:100+index}));
for(const style of ['A','B','C','D','E','F',undefined]){
const topic=style==='E';
const items=topic?list.map(item=>({id:item.id,name:'题材 '+item.id})):list;
for(const limit of [0,1,9,20]){
const sources=[{modules:[{id:1,title:'分类',style,limit,moduleType:topic?2:1,list:items,topicList:items}]},
{homeConfigData:[{id:1,name:'分类',style,limit,type:topic?3:1,status:true,list:items,themeList:items}]}];
for(const home of sources){
const html=renderHome(home).split('id="home-blocks"')[1].split('</section>')[0];
assert.equal(itemCount(html),Math.min(limit,items.length),String(style)+' '+limit);
if(limit===0)assert.ok(html.includes('暂时没有内容'));
}
}
}
});
test('missing or invalid category limits retain all valid returned items without a style cap',()=>{
const list=Array.from({length:14},(_,index)=>({...drama,id:100+index}));
for(const limit of [undefined,null,-1,1.5,NaN,Infinity,'3','',false,{},Number.MAX_SAFE_INTEGER+1]){
const html=blockSection({id:1,name:'分类',type:1,status:true,style:'B',limit,list:[null,false,...list]});
assert.equal(itemCount(html),14,String(limit));
}
const html=blockSection({id:1,name:'分类',type:1,status:true,style:'C',limit:2,list:[null,list[0],false,list[1],list[2]]});
assert.equal(itemCount(html),2);
assert.ok(html.includes('/detail/100/'));assert.ok(html.includes('/detail/101/'));assert.ok(!html.includes('/detail/102/'));
});
test('missing, reserved, malformed and incompatible category styles preserve the default content layout',()=>{
const list=Array.from({length:12},(_,index)=>({...drama,id:index+1}));
for(const style of [undefined,'','F','Z','AA',0,{},'E','A" onclick="bad']){
const html=blockSection({id:1,name:'热门好剧',type:1,status:true,style,list});
assert.ok(!html.includes('data-home-style='));assert.ok(html.includes('class="cards"'));
assert.equal((html.match(/class="hg-card"/g)||[]).length,12);
assert.ok(!html.includes('onclick='));
}
const topics=list.map(item=>({id:item.id,name:'专题'}));
const html=blockSection({id:2,name:'专题推荐',type:3,status:true,style:'D',themeList:topics});
assert.ok(!html.includes('data-home-style='));
assert.ok(html.includes('class="topic-section home-block"'));
assert.ok(!html.includes('topic-recommendations'));
assert.equal((html.match(/class="topic-recommendation-card"/g)||[]).length,12);
});
test('cover corner badges prefer 原创 over 爆剧 over 上新',()=>{
assert.equal(coverCornerBadge({isRecommend:true,isFeatured:true,isNew:true}),'<span class="cover-corner-badge cover-corner-badge--original">原创</span>');
assert.equal(coverCornerBadge({isFeatured:true,isNew:true}),'<span class="cover-corner-badge cover-corner-badge--hot">爆剧</span>');
assert.equal(coverCornerBadge({isNew:true}),'<span class="cover-corner-badge cover-corner-badge--new">上新</span>');
assert.equal(coverCornerBadge({}),'');
assert.ok(card({...drama,isRecommend:true,isFeatured:true,isNew:true}).includes('>原创</span>'));
assert.ok(!card({...drama,isRecommend:true,isFeatured:true,isNew:true}).includes('爆剧'));
assert.ok(card({...drama,isFeatured:true,isNew:true}).includes('>爆剧</span>'));
assert.ok(card({...drama,isNew:true}).includes('>上新</span>'));
});
test('styled category cards keep escaped metadata, native links and empty states',()=>{
for(const style of ['C','D']){
const html=blockSection({id:1,name:'分类',type:1,status:true,style,label:'新剧 <b>',list:[null,{...drama,desc:'简介 <script>',recommendTraceId:'trace-123',totalEpisode:36,isNew:true,tagNames:['都市 <img>']}]});
assert.ok(!html.includes('data-recommend-trace-id'));
for(const value of ['data-src="https://example.com/poster.webp"','loading="lazy"','cover-corner-badge--new','上新','36集','简介 &lt;script&gt;','都市 &lt;img&gt;'])assert.ok(html.includes(value),style+' '+value);
assert.ok(!html.includes('<script>'));assert.ok(!html.includes('<b>'));
if(style==='C'){assert.ok(html.includes('class="pick">最新上架</span>'));assert.ok(!html.includes('新剧 &lt;b&gt;'));assert.ok(!html.includes('class="pick">新剧'));}
}
for(const style of ['A','B','C','D','E']){
const html=blockSection({id:1,name:'空分类',type:style==='E'?3:1,status:true,style,list:null,themeList:[]});
assert.ok(html.includes('role="status"'));assert.ok(html.includes('暂时没有内容'));
}
});
test('nested home blocks tolerate invalid rows and non-array child lists',()=>{
const html=renderHome({homeBannerData:[null,false,drama],announcements:[null,{title:'有效公告'},4],homeConfigData:[null,7,
{id:31,name:'有效栏目',status:true,type:1,list:[null,drama]},
{id:32,name:'空视频栏目',status:true,type:2,videoList:{}},
{id:33,name:'空题材栏目',status:true,type:3,themeList:'bad'},
{id:34,name:'题材栏目',status:true,type:3,themeList:[null,{id:8,name:'悬疑'}]},
],advitise:{nineGridList:[null,advert]}});
assert.ok(!html.includes('空视频栏目'));
for(const content of ['有效栏目','空题材栏目','有效公告','href="/topics/8/"','href="/detail/23456/"','有效广告'])assert.ok(html.includes(content),content);
});
test('banner selection and card metadata tolerate malformed records without losing valid items',()=>{
assert.deepEqual(activeBanners({homeBannerData:[null,drama,{...drama,id:2,showOnBanner:true}]}).map(item=>item.id),[2]);
assert.ok(hero([null,drama]).includes('href="/detail/23456/"'));
assert.ok(hero(null).includes('暂无推荐剧集'));
assert.ok(card({...drama,tagNames:'bad',themeNames:['悬疑']}).includes('悬疑'));
assert.equal(card(null),'');assert.equal(topicCard(null),'');
});
test('carousel frames keep their own titles below the stable page heading, including fade snapshots',()=>{
const items=[drama,{...drama,id:2,title:'下一部短剧'}];
for(const selected of [0,1]){
const frame=hero(items,selected);
assert.equal((frame.match(/<h2\b/g)||[]).length,1);
assert.ok(!/<h1\b/.test(frame),'live and outgoing frames must not duplicate the page h1');
assert.ok(frame.includes(`href="/detail/${items[selected].id}/"`));
assert.ok(frame.includes(`data-feature="${selected}"`));
}
assert.ok(!/<h1\b/.test(hero(null)));
});
test('homepage landscape recommendations use enabled API content, deduplicate IDs and keep real routes and images',()=>{
const items=Array.from({length:8},(_,index)=>({...drama,id:index+1,title:'横版推荐 '+index,horizontalCover:'https://example.com/wide-'+index+'.webp',slug:'story-'+index}));
const home={homeBannerData:[{...drama,id:9}],homeConfigData:[{id:1,name:'关闭栏目',type:1,status:false,list:[{...drama,id:99}]},{id:2,name:'启用栏目',type:1,status:true,list:[items[0],items[0],null,...items.slice(1)]}]};
const recommendations=homeRecommendations(home);
assert.deepEqual(recommendations.map(item=>item.id),[1,2,3,4,5,6]);
const html=heroFeed(recommendations);
assert.equal((html.match(/class="hero-feed-card drama-data-card"/g)||[]).length,6);
assert.ok(html.includes('href="/detail/story-0/"'));assert.ok(html.includes('data-src="https://example.com/wide-0.webp"'));
assert.ok(!html.includes('关闭栏目'));assert.equal(heroFeed(null),'');
const rendered=renderHome(home);
assert.ok(rendered.includes('id="hero-feature-carousel"'));assert.ok(rendered.includes('aria-label="精选横版作品"'));
});