This commit is contained in:
谢宇宁
2026-11-06 00:13:27 +07:00
parent f65d10e998
commit 37411fcada
98 changed files with 958 additions and 4598 deletions
+27 -26
View File
@@ -1,3 +1,4 @@
import {linkShared} from './helpers/shared-modules.mjs';
import {routingModuleUrl} from './helpers/routing.mjs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
@@ -7,16 +8,16 @@ 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');
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 historyUrl=await moduleUrl('../src/lib/client/header-history.ts',code=>code.replace("'../api/protocol'",JSON.stringify(protocolUrl)).replace("'./render'",JSON.stringify(renderUrl)));
const {headerHistoryMarkup}=await import(historyUrl);
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);
@@ -25,9 +26,17 @@ test('Swagger homepage modules render actual titles, drama cards and theme desti
assert.ok(!emptyModules.includes('旧缓存不应复活'));
});
test('topic cards include escaped metadata for content exposure and click tracking',()=>{
test('topic cards escape metadata and carry no analytics attributes',()=>{
const html=topicCard({id:7,name:'都市 <甜宠>',icon:'https://example.com/topic.png'});
assert.match(html,/data-track-item data-item-id="7" data-item-type="topic" data-item-name="都市 &lt;甜宠&gt;"/);
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',()=>{
@@ -53,8 +62,8 @@ test('shared square ad rows keep real ad data and links and omit empty placement
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('data-ad-id="ad-one"'));assert.ok(html.includes('data-ad-id="ad-two"'));
assert.ok(html.includes('href="https://example.com/landing"'));assert.ok(html.includes('data-ad-type="icon"'));
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),'');
});
@@ -64,7 +73,7 @@ test('ads ignores malformed entries and keeps valid ads in order with escaped te
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('/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>'));
});
@@ -88,7 +97,7 @@ test('dynamic home sections share a grid without inheriting the prototype editor
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="drama-episodes"/g)||[]).length,1);
assert.equal((html.split('id="home-blocks"')[1].match(/class="hg-card-counter"/g)||[]).length,3);
assert.ok(html.indexOf('必看好剧')<html.indexOf('热播榜'));
});
@@ -101,7 +110,7 @@ test('homepage banner ads follow each second topic section and keep real links',
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-type="banner"'));assert.ok(blocks.includes('href="https://example.com/a"'));
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'));
});
@@ -140,7 +149,7 @@ test('A–E previews use API limits while retaining backend order and full sourc
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((html.match(/data-track-item/g)||[]).length,limit,style);
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);
@@ -166,7 +175,7 @@ test('category limits apply to current and legacy modules, including unknown sty
{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((html.match(/data-track-item/g)||[]).length,Math.min(limit,items.length),String(style)+' '+limit);
assert.equal(itemCount(html),Math.min(limit,items.length),String(style)+' '+limit);
if(limit===0)assert.ok(html.includes('暂时没有内容'));
}
}
@@ -177,10 +186,10 @@ test('missing or invalid category limits retain all valid returned items without
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((html.match(/data-track-item/g)||[]).length,14,String(limit));
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((html.match(/data-track-item/g)||[]).length,2);
assert.equal(itemCount(html),2);
assert.ok(html.includes('/detail/100/'));assert.ok(html.includes('/detail/101/'));assert.ok(!html.includes('/detail/102/'));
});
@@ -189,7 +198,7 @@ test('missing, reserved, malformed and incompatible category styles preserve the
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="card drama-data-card"/g)||[]).length,12);
assert.equal((html.match(/class="hg-card"/g)||[]).length,12);
assert.ok(!html.includes('onclick='));
}
const topics=list.map(item=>({id:item.id,name:'专题'}));
@@ -211,10 +220,11 @@ test('cover corner badges prefer 原创 over 爆剧 over 上新',()=>{
assert.ok(card({...drama,isNew:true}).includes('>上新</span>'));
});
test('styled category cards keep escaped metadata, native links, tracking and empty states',()=>{
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>']}]});
for(const value of ['data-recommend-trace-id="trace-123"','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('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">新剧'));}
}
@@ -270,12 +280,3 @@ test('homepage landscape recommendations use enabled API content, deduplicate ID
});
test('header history shows only valid unique account records, escapes titles and keeps native detail links',()=>{
const records=Array.from({length:12},(_,index)=>({...drama,id:index+1,slug:'recent-'+index,totalEpisode:index+3}));
const html=headerHistoryMarkup([null,{id:-1},records[0],records[0],...records.slice(1)]);
assert.equal((html.match(/class="jg-history-item"/g)||[]).length,9);
assert.ok(html.includes('href="/detail/recent-0/"'));assert.ok(html.includes('全 3 集'));
assert.ok(html.includes('正常剧集 &lt;script&gt;'));assert.ok(!html.includes('<script>'));
assert.ok(html.includes('data-src="https://example.com/poster.webp"'));
assert.ok(headerHistoryMarkup(null).includes('暂无观看历史'));
});