60 lines
5.1 KiB
JavaScript
60 lines
5.1 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(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('剧名 <script>'));assert.ok(!html.includes('<script>'));
|
||
assert.ok(html.includes('真实简介 <img onerror=alert(1)>'));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">剧名 <script></h1>'));assert.ok(html.includes('《剧名 <script>》剧情简介</h2>'));assert.ok(html.includes('alt="《剧名 <script>》海报"'));
|
||
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;}}
|
||
});
|