Files
web_hgcg_h5/tests/page-layouts.test.mjs
T
谢宇宁andClaude Opus 5.5 8cdbfee08f Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:23:48 +07:00

166 lines
14 KiB
JavaScript
Raw 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 renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)));
const authUrl=await moduleUrl('../src/lib/client/auth.ts');
const accountUrl=await moduleUrl('../src/lib/client/account.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)).replace('"./render"',JSON.stringify(renderUrl)).replace("'./auth'",JSON.stringify(authUrl)));
const feedbackUrl=await moduleUrl('../src/lib/client/feedback.ts',code=>code.replace('"./render"',JSON.stringify(renderUrl)));
const {card}=await import(renderUrl);
const {accountMenuMarkup,settingsMarkup,memberMarkup,renderMember}=await import(accountUrl);
const {feedbackPayload,feedbackMarkup,renderFeedbackPage}=await import(feedbackUrl);
const flush=()=>new Promise(resolve=>setImmediate(resolve));
test('ranking cards retain original poster frames and global positions, and search highlights only escaped titles',()=>{
const drama={id:7,title:'测试 <短剧>',cover:'https://cdn.example/cover.webp',totalEpisode:10};
const ranked=card(drama,false,{rank:25});
assert.ok(ranked.includes('class="ranking-poster-frame"'));assert.ok(ranked.includes('data-rank="25"'));assert.ok(ranked.includes('第 25 名'));assert.ok(ranked.includes('>25</span>'));
assert.ok(ranked.includes('href="/detail/7/"'));assert.ok(ranked.includes('data-src="https://cdn.example/cover.webp"'));
assert.ok(card(drama,false,{rank:1}).includes('>01</span>'));
assert.ok(!card(drama,false,{rank:-1}).includes('rank-home-badge'));
const search=card(drama,false,{keyword:'<短剧>'});
assert.ok(search.includes('<span class="search-keyword">&lt;短剧&gt;</span>'));assert.ok(!search.includes('<短剧>'));
assert.ok(search.includes('aria-label="查看测试 &lt;短剧&gt;"'));
});
test('personal pages use actual library data and route links inside the original summary and tabs',async()=>{
const oldDocument=globalThis.document,root={innerHTML:''},calls=[];
const user={userId:88,nickName:'真实昵称 <script>',recharge:2400,avatar:'https://cdn.example/avatar.webp'};
const app={signal:new AbortController().signal,home:{},api:{ensureSession:async()=>user,get:async(path,query)=>{calls.push({path,query});return {list:[{id:7,title:'已看过的短剧'}],totalCount:72};}}};
globalThis.document={getElementById:()=>root};
try{
await renderMember(app,'center',1,()=>'/member/center/');
assert.equal(calls.at(-1).path,'playlet/history/list');
for(const className of ['member-name-row','member-settings-link','member-wallet','coin-soft-star','member-library-tabs','member-library-toolbar','member-library-cards','member-library-item'])assert.ok(root.innerHTML.includes(className),className);
assert.ok(root.innerHTML.includes('真实昵称 &lt;script&gt;'));assert.ok(root.innerHTML.includes('2,400'));assert.ok(root.innerHTML.includes('ID:88'));
assert.ok(root.innerHTML.includes('href="/member/settings/"'));assert.ok(root.innerHTML.includes('href="/member/history/page/2/"'));assert.ok(!root.innerHTML.includes('/member/center/page/'));
assert.ok(root.innerHTML.includes('href="/member/favorites/"'));assert.ok(root.innerHTML.includes('href="/member/likes/"'));assert.ok(root.innerHTML.includes('href="/member/purchased/"'));
await renderMember(app,'favorites',2,page=>`/member/favorites/page/${page}/`);
assert.equal(calls.at(-1).path,'playlet/collect/list');assert.equal(calls.at(-1).query.pageNum,2);
assert.ok(root.innerHTML.includes('href="/member/favorites/" aria-current="page"'));
assert.ok(!root.innerHTML.includes('吴秀英'));assert.ok(!root.innerHTML.includes('1,280'));
}finally{globalThis.document=oldDocument;}
});
test('likes use the protected list endpoint, native pagination and the reference library markup',async()=>{
const previous=globalThis.document,root={innerHTML:''},calls=[];
const user={userId:8,nickName:'点赞用户'};
const app={signal:new AbortController().signal,home:{},api:{ensureSession:async()=>user,get:async(path,query)=>{
calls.push({path,query});return {list:[{id:7,slug:'liked-story',title:'点赞 <短剧>',totalEpisode:8}],totalCount:72};
}}};
globalThis.document={getElementById:()=>root};
try{
await renderMember(app,'likes',2,page=>page===1?'/member/likes/':`/member/likes/page/${page}/`);
assert.deepEqual(calls.at(-1),{path:'playlet/like/list',query:{pageNum:2,pageSize:24}});
assert.ok(root.innerHTML.includes('href="/member/likes/" aria-current="page"><h2 id="member-library-title"'));
assert.ok(root.innerHTML.includes('共 72 部短剧'));assert.ok(root.innerHTML.includes('href="/member/likes/page/3/"'));
assert.ok(root.innerHTML.includes('href="/detail/liked-story/"'));assert.ok(root.innerHTML.includes('点赞 &lt;短剧&gt;'));
for(const tab of ['history','favorites','likes','purchased'])assert.ok(root.innerHTML.includes(`href="/member/${tab}/"`));
const initial={user,list:{list:[],totalCount:0}};calls.length=0;
await renderMember(app,'likes',1,()=>'/member/likes/',initial);
assert.equal(calls.length,0,'SSR data must be reused without a second list request');
assert.ok(root.innerHTML.includes('member-library-empty'));assert.ok(root.innerHTML.includes('暂无点赞的短剧'));
assert.ok(root.innerHTML.includes('href="/shorts/">去发现好剧</a>'));
assert.ok(!memberMarkup(user,'likes',{list:null,totalCount:0}).includes('undefined'));
}finally{globalThis.document=previous;}
});
test('the account menu exposes a native likes entry for guests and registered users',()=>{
for(const user of [null,{userId:8,nickName:'用户 <script>',isRegister:true}]){
const html=accountMenuMarkup(user);
assert.ok(html.includes('<a href="/member/likes/">我的点赞 ›</a>'));
assert.ok(html.indexOf('/member/favorites/')<html.indexOf('/member/likes/'));
assert.ok(html.indexOf('/member/likes/')<html.indexOf('/member/history/'));
assert.ok(!html.includes('<script>'));
}
});
test('settings preserve actual profile fields and auth actions inside the reference cards',()=>{
const html=settingsMarkup({userId:8,nickName:'用户 <script>',introduction:'简介 <img>',avatar:'https://cdn.example/avatar.webp',isRegister:true});
for(const text of ['member-settings-card','member-avatar-editor','member-avatar-preview','member-nickname-editor','账号与安全','data-auth-mode="register"','data-auth-mode="switch"','data-logout','id="security-form"','name="oldPassword"','name="newPassword"'])assert.ok(html.includes(text),text);
assert.ok(html.includes('value="用户 &lt;script&gt;"'));assert.ok(html.includes('简介 &lt;img&gt;'));assert.ok(html.includes('已绑定'));
assert.ok(html.includes('id="member-avatar-file" accept="image/png,image/jpeg,image/webp" hidden'));assert.ok(html.includes('data-src="https://cdn.example/avatar.webp"'));
});
test('feedback options map to the real API type and keep environment details with actual attachments',()=>{
const html=feedbackMarkup();
for(const text of ['feedback-reference','feedback-options','feedback-panel','feedback-description-wrap','feedback-upload-box','feedback-submit-button'])assert.ok(html.includes(text),text);
assert.ok(!html.includes('video/mp4'));assert.ok(!html.includes('保存到本地'));
const data=new FormData();for(const [key,value] of Object.entries({category:'登录异常',content:'播放时一直缓冲,无法正常观看视频。',contact:' me@example.com ',network:'Wi-Fi',device:'iPhone'}))data.set(key,value);
const body=feedbackPayload(data,['/files/real.png']);assert.equal(body.type,4);assert.equal(body.contact,'me@example.com');assert.deepEqual(body.screenshots,['/files/real.png']);
assert.ok(body.content.includes('网络类型:Wi-Fi'));assert.ok(body.content.includes('设备类型:iPhone'));
data.set('category','无法播放');assert.equal(feedbackPayload(data).type,2);data.set('category','其他问题');assert.equal(feedbackPayload(data).type,5);
data.set('content','长'.repeat(500));assert.equal(feedbackPayload(data).content,'长'.repeat(500));
data.set('content','中'.repeat(490));assert.equal(feedbackPayload(data).content.length,500);
data.set('content','太短');assert.throws(()=>feedbackPayload(data),/10–500/);
});
class Element extends EventTarget {
nodes=new Map();dataset={};innerHTML='';textContent='';disabled=false;value='';files=[];
querySelector(selector){return this.nodes.get(selector)||null;}
querySelectorAll(){return [];}
reportValidity(){return true;}
reset(){this.resetCount=(this.resetCount||0)+1;}
}
function emit(element,type){element.dispatchEvent(new Event(type,{cancelable:true}));}
test('restyled settings forms retain distinct profile/password payloads and prevent repeated saves',async()=>{
const oldDocument=globalThis.document,OldFormData=globalThis.FormData;
const controller=new AbortController(),root=new Element(),profile=new Element(),security=new Element();
const values=new Map([[profile,{nickName:'新昵称',introduction:'新的简介'}],[security,{oldPassword:'Old12345',newPassword:'New12345'}]]);
for(const form of [profile,security]){form.nodes.set('[type="submit"]',new Element());form.nodes.set('.api-error',new Element());}
root.nodes.set('#profile-form',profile);root.nodes.set('#security-form',security);
const calls=[];let finish,refreshes=0,routes=0;
const app={signal:controller.signal,home:{},api:{ensureSession:async()=>({userId:8}),get:async()=>({}),post:async(path,body)=>{calls.push({path,body});if(calls.length===1)await new Promise(resolve=>finish=resolve);},refreshUser:async()=>refreshes++},notify(){},async route(){routes++;}};
globalThis.document={getElementById:()=>root};globalThis.FormData=class extends OldFormData{constructor(form){super();if(form)for(const [key,value] of Object.entries(values.get(form)))this.set(key,value);}};
try{
await renderMember(app,'settings',1,()=>'/member/settings/');
emit(profile,'submit');emit(profile,'submit');assert.equal(calls.length,1);assert.equal(profile.querySelector('[type="submit"]').disabled,true);
finish();await flush();assert.deepEqual(calls[0],{path:'auth/profile',body:{nickName:'新昵称',introduction:'新的简介'}});assert.equal(refreshes,1);assert.equal(routes,1);
emit(security,'submit');await flush();assert.deepEqual(calls[1],{path:'auth/profile',body:{oldPassword:'Old12345',newPassword:'New12345'}});assert.equal(refreshes,2);assert.equal(routes,1);
}finally{controller.abort();globalThis.document=oldDocument;globalThis.FormData=OldFormData;}
});
function feedbackFixture(post){
const oldDocument=globalThis.document,OldFormData=globalThis.FormData;
const controller=new AbortController(),root=new Element(),form=new Element(),input=new Element(),list=new Element(),status=new Element(),error=new Element(),content=new Element(),count=new Element(),submit=new Element();
const values={content:'播放视频时持续缓冲,无法正常观看。',category:'无法播放',contact:'me@example.com',network:'Wi-Fi'};
root.nodes.set('#feedback-form',form);
for(const [selector,node] of [['#feedback-files',input],['#feedback-file-list',list],['#feedback-status',status],['#feedback-file-error',error],['[name="content"]',content],['#feedback-count',count],['[type="submit"]',submit]])form.nodes.set(selector,node);
globalThis.document={getElementById:()=>root};globalThis.FormData=class extends OldFormData{constructor(form){super();if(form)for(const [key,value] of Object.entries(values))this.set(key,value);}};
const calls=[];renderFeedbackPage({signal:controller.signal,api:{post:async(path,body)=>{calls.push({path,body});return post(path,body);}}});
return {controller,form,input,list,status,error,submit,calls,dispose(){controller.abort();globalThis.document=oldDocument;globalThis.FormData=OldFormData;}};
}
test('reference feedback form uploads images only on submit and blocks duplicate feedback writes',async()=>{
let finish;const f=feedbackFixture(path=>path==='upload/image'?new Promise(resolve=>finish=resolve):Promise.resolve({}));
try{
f.input.files=[new File(['image'],'shot.png',{type:'image/png'})];emit(f.input,'change');assert.equal(f.calls.length,0);assert.ok(f.list.innerHTML.includes('shot.png'));
emit(f.form,'submit');emit(f.form,'submit');assert.equal(f.calls.length,1);assert.equal(f.calls[0].path,'upload/image');assert.equal(f.submit.disabled,true);
finish({sourceNameFs:'/uploaded/shot.png'});await flush();
assert.equal(f.calls.length,2);assert.equal(f.calls[1].path,'feedback/add');assert.deepEqual(f.calls[1].body.screenshots,['/uploaded/shot.png']);assert.equal(f.calls[1].body.type,2);
assert.equal(f.form.resetCount,1);assert.match(f.status.textContent,/反馈已提交/);assert.equal(f.submit.disabled,false);
}finally{f.dispose();}
});
test('unsupported files, failed uploads and navigation cannot silently submit feedback',async()=>{
const failed=feedbackFixture(async()=>({}));
try{
failed.input.files=[new File(['bad'],'clip.mp4',{type:'video/mp4'})];emit(failed.input,'change');assert.match(failed.error.textContent,/JPG/);assert.equal(failed.calls.length,0);
failed.input.files=[new File(['valid'],'shot.png',{type:'image/png'})];emit(failed.input,'change');emit(failed.form,'submit');await flush();
assert.deepEqual(failed.calls.map(call=>call.path),['upload/image']);assert.match(failed.status.textContent,/上传失败/);assert.equal(failed.submit.disabled,false);assert.equal(failed.form.resetCount,undefined);
}finally{failed.dispose();}
let finish;const late=feedbackFixture(()=>new Promise(resolve=>finish=resolve));
try{
late.input.files=[new File(['valid'],'shot.png',{type:'image/png'})];emit(late.input,'change');emit(late.form,'submit');late.controller.abort();finish({sourceNameFs:'/uploaded/late.png'});await flush();
assert.deepEqual(late.calls.map(call=>call.path),['upload/image']);assert.equal(late.form.resetCount,undefined);
}finally{late.dispose();}
});