@@ -0,0 +1,220 @@
|
||||
import {routingModuleUrl} from './helpers/routing.mjs';
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {File as NodeFile} from 'node:buffer';
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import ts from 'typescript';
|
||||
|
||||
if(typeof globalThis.File==='undefined')globalThis.File=NodeFile;
|
||||
|
||||
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 trackingUrl=await moduleUrl('../src/lib/tracking.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)).replace("'../tracking'",JSON.stringify(trackingUrl)));
|
||||
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'));
|
||||
const icons=Array.from({length:18},(_,index)=>({id:`icon-${index}`,title:`图标${String(index).padStart(2,'0')}`,images:`https://cdn.example/${index}.png`,link:`https://example.com/${index}`}));
|
||||
const marked=memberMarkup(user,'center',{list:[],totalCount:0},{advitise:{nineGridList:[null,{id:'bad',title:'坏图',images:'javascript:alert(1)'},...icons]}});
|
||||
assert.equal((marked.match(/class="member-ad-tile"/g)||[]).length,16);
|
||||
assert.ok(marked.indexOf('member-ad-grid')<marked.indexOf('member-summary'));
|
||||
assert.ok(marked.includes('图标00'));assert.ok(marked.includes('图标15'));
|
||||
assert.ok(!marked.includes('图标16'));assert.ok(!marked.includes('javascript:'));
|
||||
assert.ok(marked.includes('data-ad-type="icon"'));assert.ok(marked.includes('width="76"'));
|
||||
assert.ok(!memberMarkup(user,'center',{}).includes('member-ad-grid'));
|
||||
assert.ok(!memberMarkup(user,'settings',{},{advitise:{nineGridList:icons}}).includes('member-ad-tile'));
|
||||
}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('href="/member/likes/"'));
|
||||
assert.ok(html.includes('我的点赞'));
|
||||
assert.ok(html.includes('class="profile-row"'));
|
||||
assert.ok(html.includes('account-collection-icon'));
|
||||
assert.ok(html.includes('data-auth-mode="login"'));
|
||||
assert.ok(html.includes('data-auth-mode="bind"'));
|
||||
assert.ok(html.includes('class="profile-switch"'));
|
||||
assert.ok(html.indexOf('/member/favorites/')<html.indexOf('/member/likes/'));
|
||||
assert.ok(html.indexOf('/member/likes/')<html.indexOf('/member/history/'));
|
||||
assert.ok(html.indexOf('/member/history/')<html.indexOf('/info/deposit/'));
|
||||
assert.ok(html.indexOf('/info/deposit/')<html.indexOf('data-auth-mode="switch"'));
|
||||
assert.ok(!html.includes('已购剧集'));
|
||||
assert.ok(!html.includes('账号设置'));
|
||||
assert.ok(!html.includes('退出登录'));
|
||||
assert.ok(!html.includes('data-logout'));
|
||||
assert.ok(!html.includes('<script>'));
|
||||
if(user)assert.ok(html.includes('用户 <script>'));
|
||||
}
|
||||
const counted=accountMenuMarkup({userId:8,nickName:'甲',recharge:1280},{collectCount:3,likeCount:5});
|
||||
assert.ok(counted.includes('id="profile-saved-count">3'));
|
||||
assert.ok(counted.includes('id="profile-likes-count">5'));
|
||||
assert.ok(!counted.includes('profile-watch-count'));
|
||||
assert.ok(!counted.includes('profile-bought-count'));
|
||||
assert.ok(counted.includes('id="profile-coin-count">1,280')||counted.includes('id="profile-coin-count">1280'));
|
||||
assert.ok(!counted.includes('data-logout'));
|
||||
});
|
||||
|
||||
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,hasPassword:true});
|
||||
for(const text of ['member-settings-card','member-nickname-editor','id="member-nickname-form"','账号与安全','data-member-settings="bind"','data-auth-mode="register"','data-member-settings="switch"','data-auth-mode="switch"','ui-chevron-right','maxlength="20"','支持 1–20 个字符'])assert.ok(html.includes(text),text);
|
||||
assert.ok(html.includes('value="用户 <script>"'));assert.ok(html.includes('data-bind-state>已绑定'));
|
||||
assert.ok(settingsMarkup({userId:9,nickName:'游客',type:0,isRegister:false,hasPassword:false}).includes('data-bind-state>未绑定'));
|
||||
assert.ok(settingsMarkup({userId:10,nickName:'会员',type:1}).includes('data-bind-state>已绑定'));
|
||||
assert.ok(!html.includes('个人简介'));assert.ok(!html.includes('data-logout'));
|
||||
assert.ok(!html.includes('修改密码'));assert.ok(!html.includes('id="security-form"'));assert.ok(!html.includes('保存密码'));
|
||||
assert.ok(html.includes('class="member-settings-avatar"'));assert.ok(html.includes('alt="当前头像"'));assert.ok(html.includes('data-src="https://cdn.example/avatar.webp"'));
|
||||
assert.ok(!html.includes('更换头像'));assert.ok(!html.includes('member-avatar-file'));assert.ok(!html.includes('data-avatar-action'));
|
||||
});
|
||||
|
||||
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,3);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,4);data.set('category','闪退 / 崩溃');assert.equal(feedbackPayload(data).type,4);
|
||||
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('nickname settings save the nickname once and keep an existing introduction',async()=>{
|
||||
const oldDocument=globalThis.document,OldFormData=globalThis.FormData;
|
||||
const controller=new AbortController(),root=new Element(),profile=new Element(),status=new Element();
|
||||
const values=new Map([[profile,{nickname:'新昵称'}]]);
|
||||
profile.nodes.set('[type="submit"]',new Element());profile.nodes.set('#member-nickname-status',status);
|
||||
root.nodes.set('#member-nickname-form',profile);
|
||||
const calls=[];let finish,refreshes=0,routes=0;
|
||||
const app={signal:controller.signal,home:{},api:{ensureSession:async()=>({userId:8,introduction:'原简介'}),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);
|
||||
}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('later feedback images are kept alongside earlier ones until three are selected',()=>{
|
||||
const f=feedbackFixture(async()=>({}));
|
||||
const image=(name)=>new File(['image'],name,{type:'image/png'});
|
||||
try{
|
||||
f.input.files=[image('one.png')];emit(f.input,'change');
|
||||
f.input.files=[image('two.png')];emit(f.input,'change');
|
||||
assert.ok(f.list.innerHTML.includes('one.png'));assert.ok(f.list.innerHTML.includes('two.png'));assert.equal(f.error.textContent,'');
|
||||
f.input.files=[image('three.png'),image('four.png')];emit(f.input,'change');
|
||||
assert.match(f.error.textContent,/最多上传 3 张/);assert.ok(!f.list.innerHTML.includes('three.png'));assert.ok(f.list.innerHTML.includes('one.png'));
|
||||
f.input.files=[image('three.png')];emit(f.input,'change');
|
||||
assert.equal(f.error.textContent,'');assert.ok(f.list.innerHTML.includes('three.png'));assert.equal(f.input.disabled,true);
|
||||
f.list.closest=()=>({dataset:{removeFeedbackFile:'0'}});
|
||||
f.list.dispatchEvent(new Event('click'));
|
||||
assert.ok(!f.list.innerHTML.includes('one.png'));assert.ok(f.list.innerHTML.includes('two.png'));assert.equal(f.input.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