Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-27 16:23:48 +07:00
co-authored by Claude Opus 5.5
commit 8cdbfee08f
345 changed files with 20773 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
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 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('overview retains API data, encrypted image loading and real chapter links in the original layout',()=>{
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('古装 · 爱情'));
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('series-badge--new'));
assert.ok(html.includes('<div class="series-action-bar"><div class="series-actions">'));
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 use configured safe URLs and retain the original feedback 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','https://contact.example/'])assert.ok(html.includes(`href="${url}"`));
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('href="/info/feedback/"'));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','feedback']){
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;}}
});