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

71 lines
6.1 KiB
JavaScript

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(file,replace=code=>code){
const filename=new URL(file,import.meta.url);
const {outputText}=ts.transpileModule(await readFile(filename,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}});
return 'data:text/javascript;base64,'+Buffer.from(linkShared(replace(outputText)).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))+'\n//# sourceURL='+filename.pathname).toString('base64');
}
const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts');
const renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)));
const detailUrl=await moduleUrl('../src/lib/client/detail.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)).replace('"./render"',JSON.stringify(renderUrl)));
const {overviewMarkup,officialLinks,bindSeriesDescription,detailActionIcon}=await import(detailUrl);
test('cinematic overview keeps API data, encrypted image loading and real chapter links',()=>{
const html=overviewMarkup({id:23456,title:'剧名 <script>',coverImg:'https://cdn.example/encrypted.webp',description:'真实简介 <img onerror=alert(1)>',themeNames:['古装','爱情'],totalEpisode:12,updateStatus:1,isNew:true,countCollect:23000,countLike:100,heatValue:999},{id:90042,currentEpisode:1});
assert.ok(html.includes('剧名 &lt;script&gt;'));assert.ok(!html.includes('<script>'));
assert.ok(html.includes('真实简介 &lt;img onerror=alert(1)&gt;'));assert.ok(html.includes('#古装'));assert.ok(html.includes('#爱情'));
assert.ok(html.includes('class="cine-hero"'));assert.ok(html.includes('class="cine-hero-image"'));assert.ok(!html.includes('series-poster'));
assert.ok(html.includes('data-src="https://cdn.example/encrypted.webp"'));assert.ok(!/\ssrc="https:\/\/cdn\.example/.test(html));
assert.ok(html.includes('<span class="cine-kicker">短剧 · 连载中</span>'));assert.ok(html.includes('<h1 id="play-detail-title">'));
assert.ok(html.includes('href="/play/23456/90042/"'));assert.ok(html.includes('<span>立即观看</span>'));
for(const text of ['999热度','2.3万收藏','100点赞','data-engage="like"','data-engage="collect"','data-share'])assert.ok(html.includes(text),text);
assert.ok(html.includes('aria-controls="series-description-text"'));assert.ok(html.includes('data-app-download-entry'));
assert.ok(!overviewMarkup({id:7,title:'无章节'},undefined).includes('href="/play/'));
const later=overviewMarkup({id:9,title:'从中间开始',totalEpisode:30},{id:5,currentEpisode:3});
assert.ok(later.includes('从第 3 集开始'));
const single=overviewMarkup({id:8,title:'单集',totalEpisode:1,tagNames:['家庭'],tagSlugs:['jiating'],themeNames:['家庭']},{id:1});
assert.ok(single.includes('<span class="cine-kicker">视频</span>'));assert.ok(!single.includes('全1集'));
assert.ok(single.includes('href="/tag/jiating/"'));assert.equal(single.split('#家庭').length-1,1);
});
test('official links use configured safe URLs without a customer-service entrance',()=>{
const html=officialLinks({config:{newUrlAddr:'https://entry.example',foreverUrlAddr:'https://permanent.example',tgGroup:'https://t.me/example',telegram:'https://contact.example'}});
for(const url of ['https://entry.example/','https://permanent.example/','https://t.me/example'])assert.ok(html.includes(`href="${url}"`));
assert.ok(html.includes('点击加入黄果短剧官方 Telegram 群'));
for(const text of ['官方客服','https://contact.example','/info/feedback/'])assert.ok(!html.includes(text),text);
const fallback=officialLinks({config:{newUrlAddr:'javascript:alert(1)',tgGroup:'javascript:alert(1)'},navPageList:[{line:1,url:'https://backup.example'}]});
assert.ok(fallback.includes('href="https://backup.example/"'));assert.ok(!fallback.includes('javascript:'));
assert.ok(!fallback.includes('huangguo.me'));assert.ok(!fallback.includes('huangguo.tv'));
});
test('original action SVGs remain accessible and inherit the active colors',()=>{
for(const kind of ['like','save','share']){
const html=detailActionIcon(kind);assert.match(html,/<svg class="play-action-icon"/);assert.match(html,/aria-hidden="true"/);assert.ok(html.includes('<path d="'));
}
});
test('description expands only when overflowing three lines and resets when the layout becomes wider',()=>{
const originals={getComputedStyle:globalThis.getComputedStyle,ResizeObserver:globalThis.ResizeObserver};
let update,disconnects=0;
const classes=new Set(),attributes=new Map();
const text={clientWidth:200,scrollHeight:160,classList:{remove:key=>classes.delete(key),toggle:(key,on)=>on?classes.add(key):classes.delete(key)}};
const toggle=new EventTarget();toggle.hidden=true;toggle.textContent='展开';toggle.getAttribute=key=>attributes.get(key);toggle.setAttribute=(key,value)=>attributes.set(key,value);
const root={querySelector:selector=>selector==='#series-description-text'?text:toggle};
const controller=new AbortController();
globalThis.getComputedStyle=()=>({lineHeight:'24px'});
globalThis.ResizeObserver=class {constructor(callback){update=callback;}observe(){}disconnect(){disconnects++;}};
try{
bindSeriesDescription(root,controller.signal);assert.equal(toggle.hidden,false);
toggle.dispatchEvent(new Event('click'));assert.equal(classes.has('is-expanded'),true);assert.equal(toggle.textContent,'收起');
toggle.dispatchEvent(new Event('click'));assert.equal(classes.has('is-expanded'),false);assert.equal(toggle.textContent,'展开');
text.scrollHeight=60;update();assert.equal(toggle.hidden,true);assert.equal(attributes.get('aria-expanded'),'false');
text.scrollHeight=160;update();assert.equal(toggle.hidden,false);
controller.abort();assert.equal(disconnects,1);toggle.dispatchEvent(new Event('click'));assert.equal(classes.has('is-expanded'),false);
}finally{for(const [key,value] of Object.entries(originals)){if(value===undefined)delete globalThis[key];else globalThis[key]=value;}}
});