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:'正常剧集 ',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 include escaped metadata for content exposure and click tracking',()=>{ 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="都市 <甜宠>"/); }); 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(/]*>/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&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('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('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:'第二条 ')); }); 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="drama-episodes"/g)||[]).length,1); assert.ok(html.indexOf('必看好剧'){ 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(/