Files
2026-10-29 16:25:57 +07:00

128 lines
8.2 KiB
JavaScript

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(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,card,topicCard,imageAttributes}=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'};
test('topic cards escape their names and carry no tracking attributes',()=>{
const html=topicCard({id:7,name:'都市 <甜宠>',icon:'https://example.com/topic.png'});
assert.ok(html.includes('<h3>都市 &lt;甜宠&gt;</h3>')||html.includes('都市 &lt;甜宠&gt;'));assert.doesNotMatch(html,/data-track|data-item-/);
});
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="\/assets\/poster-placeholder\.svg"/);assert.match(tag,/\sdata-src="https:\/\//);
assert.ok(!/\ssrc="https?:/.test(tag));
}
}
assert.match(imageAttributes(drama.cover,undefined,'poster'),/^poster="\/assets\/poster-placeholder\.svg" 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.equal((html.match(/data-ad href=/g)||[]).length,2);assert.doesNotMatch(html,/data-track|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('href="/apps/"'));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'));
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.match(/class="hg-card-eps"/g)||[]).length,1);
assert.ok(html.indexOf('必看好剧')<html.indexOf('热播榜'));
});
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('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('ad links carry the numeric ad ID and slot for click reporting, and invalid identities are omitted',()=>{
const html=promotionRow([{id:'1138200002',location:2,title:'广告',images:'https://example.com/ad.png',link:'https://example.com/landing'},{id:'bad-id',location:2,title:'无效',images:'https://example.com/b.png',link:'https://example.com/b'}]);
assert.ok(html.includes('data-ad data-ads-id="1138200002" data-ads-location="2" href="https://example.com/landing"'));
assert.equal((html.match(/data-ads-id=/g)||[]).length,1);
});