Files
2026-10-27 17:39:27 +07:00

60 lines
5.1 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 {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(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 siteUrl=await moduleUrl('../src/lib/site.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)).replace("'../site'",JSON.stringify(siteUrl)));
const {overviewMarkup,officialLinks,bindSeriesDescription,detailActionIcon}=await import(detailUrl);
test('overview exposes H1, labelled facts, linked genres, synopsis heading and real chapter links',()=>{
const html=overviewMarkup({id:23456,title:'剧名 <script>',coverImg:'https://cdn.example/encrypted.webp',description:'真实简介 <img onerror=alert(1)>',themeNames:['古装','爱情'],isNew:true,countCollect:23000,countLike:100,heatValue:999},{id:90042,currentEpisode:1},'<div class="play-detail-actions">actions</div>');
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('<dt>类型</dt>'));assert.ok(html.includes('<span>古装</span><span>爱情</span>'));assert.ok(html.includes('<dt>集数</dt>'));
assert.ok(html.includes('data-src="https://cdn.example/encrypted.webp"'));assert.ok(!/\ssrc="https:\/\/cdn\.example/.test(html));
assert.ok(html.includes('href="/play/23456/90042/"'));assert.ok(html.includes('hg-detail-badge--new'));assert.ok(html.includes('<h1 id="play-detail-title">剧名 &lt;script&gt;</h1>'));assert.ok(html.includes('《剧名 &lt;script&gt;》剧情简介</h2>'));assert.ok(html.includes('alt="《剧名 &lt;script&gt;》海报"'));
assert.ok(html.includes('<div class="play-detail-actions">actions</div>'));
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/'));
});
test('official links show only the site domain and the official TG, ignoring configured mirrors and contacts',()=>{
const html=officialLinks();
assert.ok(html.includes('href="https://huangguocg.com/"'));assert.ok(html.includes('官方TG:'));assert.ok(html.includes('href="https://t.me/jbd125"'));assert.ok(html.includes('>@jbd125</a>'));
for(const text of ['官方客服','Telegram 群','javascript:'])assert.ok(!html.includes(text),text);
});
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;}}
});