@@ -0,0 +1,165 @@
|
||||
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"><短剧></span>'));assert.ok(!search.includes('<短剧>'));
|
||||
assert.ok(search.includes('aria-label="查看测试 <短剧>"'));
|
||||
});
|
||||
|
||||
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('真实昵称 <script>'));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('点赞 <短剧>'));
|
||||
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="用户 <script>"'));assert.ok(html.includes('简介 <img>'));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();}
|
||||
});
|
||||
Reference in New Issue
Block a user