Files
creator-hub/web/tests/event-aggregation-page.test.cjs
T

170 lines
13 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { resolve } = require('node:path');
const Module = require('node:module');
const ts = require('typescript');
const { loadPageState } = require('./helpers/page-state.cjs');
const React = require('react');
function transpile(file) { return ts.transpileModule(readFileSync(file,'utf8'), {compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX,target:ts.ScriptTarget.ES2020}}).outputText; }
function harness(options={}) {
const states=[],effects=[],queued=[],calls=[];
let cursor=0;
const antd={};
for(const name of ['Alert','Avatar','Button','Card','Flex','Select','Table','Tag','Tooltip']){antd[name]=()=>null;antd[name].displayName=name;}
antd.Typography={Text:()=>null,Paragraph:()=>null,Link:()=>null};
const utilityFile=resolve(__dirname,'../src/utils/account-events.ts');
const utility=new Module(utilityFile,module);utility._compile(transpile(utilityFile),utilityFile);
const file=resolve(__dirname,'../src/pages/creator/events/index.tsx');
const loaded=new Module(file,module);loaded.filename=file;
loaded.require=id=>{
if(id==='react')return{useMemo:factory=>factory(),useCallback:callback=>callback,useState(initial){const index=cursor++;if(!(index in states))states[index]=initial;return[states[index],value=>{states[index]=typeof value==='function'?value(states[index]):value;}]},
useEffect(callback,deps){const index=cursor++;const prior=effects[index];if(!prior||deps.some((v,i)=>!Object.is(v,prior.deps[i]))){if(prior?.cleanup)prior.cleanup();effects[index]={deps};queued.push(()=>{effects[index].cleanup=callback();});}}};
if(id==='react/jsx-runtime')return require(id);
if(id==='antd')return antd;
if(id==='@ant-design/icons')return{ReloadOutlined:()=>null,VideoCameraOutlined:()=>null};
if(id==='@/components/creator/WorksGallery')return{WorkCover:Object.assign(()=>null,{displayName:'WorkCover'})};
if(id==='@/utils/account-events')return utility.exports;
if(id==='@/hooks/usePageState')return loadPageState(loaded.require('react'),{storage:options.storage});
if(id==='@/utils/table')return{useTableSticky:()=>false,tablePagination:()=>({})};
if(id==='@/utils/helpers')return{dateTime:value=>`time:${value}`,conflictMessage:(cause,fallback)=>cause.message||fallback};
if(id==='@/services/api')return{creatorGet:async path=>{calls.push(path);if(options.get)return options.get(path);if(path==='/creator/accounts')return options.accounts||[];return options.page||{data:[],total:0};}};
return require(id);
};
loaded._compile(transpile(file),file);
return{antd,calls,render(){cursor=0;return loaded.exports.default();},async effects(){while(queued.length)queued.shift()();for(let i=0;i<8;i++)await Promise.resolve();},dispose(){for(const effect of effects)effect?.cleanup?.();}};
}
function all(node,predicate){const result=[];function visit(value){if(Array.isArray(value))return value.forEach(visit);if(!React.isValidElement(value))return;if(predicate(value))result.push(value);visit(value.props.children);}visit(node);return result;}
function text(node){if(node==null)return'';if(typeof node==='string'||typeof node==='number')return String(node);if(Array.isArray(node))return node.map(text).join('');return React.isValidElement(node)?text(node.props.children):'';}
function select(h,tree,label){return all(tree,node=>node.type===h.antd.Select&&node.props['aria-label']===label)[0];}
function table(h,tree){return all(tree,node=>node.type===h.antd.Table)[0];}
test('event aggregation restores pagination and filters together after returning', async t => {
const storage = new Map();
const options = {storage, accounts:[{id:'on',name:'账号',listener_enabled:true}],page:{data:[],total:150}};
const first = harness(options); t.after(()=>first.dispose());
first.render(); await first.effects(); let tree = first.render();
select(first,tree,'所属账号').props.onChange('on');
select(first,tree,'事件类型').props.onChange('comment');
select(first,tree,'接收时间范围').props.onChange(6);
first.render(); await first.effects(); tree = first.render();
table(first,tree).props.pagination.onChange(1,50);
first.render(); await first.effects(); tree = first.render();
table(first,tree).props.pagination.onChange(3,50);
first.render(); await first.effects();
const returned = harness(options); t.after(()=>returned.dispose());
returned.render(); await returned.effects(); tree = returned.render();
assert.equal(table(returned,tree).props.pagination.current,3);
assert.equal(table(returned,tree).props.pagination.pageSize,50);
assert.equal(select(returned,tree,'所属账号').props.value,'on');
assert.equal(select(returned,tree,'事件类型').props.value,'comment');
assert.equal(select(returned,tree,'接收时间范围').props.value,6);
const query = new URLSearchParams(returned.calls.find(url=>url.startsWith('/creator/events')).split('?')[1]);
assert.equal(query.get('page'),'3'); assert.equal(query.get('page_size'),'50');
assert.equal(query.get('account_id'),'on'); assert.equal(query.get('event_type'),'comment');
select(returned,tree,'接收时间范围').props.onChange(12);
returned.render(); await returned.effects(); tree = returned.render();
assert.equal(table(returned,tree).props.pagination.current,1);
});
test('only enabled accounts are offered, event filters are server-wide and changes reset page',async t=>{
const h=harness({accounts:[{id:'on',name:'开启账号',platform_account_key:'1',listener_enabled:true},{id:'off',name:'关闭账号',listener_enabled:false}],page:{data:[{id:'event'}],total:40}});t.after(()=>h.dispose());
h.render();await h.effects();let tree=h.render();
assert.deepEqual(select(h,tree,'所属账号').props.options.map(o=>o.value),['on']);
assert.equal(select(h,tree,'所属账号').props.options[0].label,'开启账号');
assert.equal(table(h,tree).props.pagination.total,40);
table(h,tree).props.pagination.onChange(2,25);h.render();await h.effects();tree=h.render();
assert.equal(table(h,tree).props.pagination.current,2);
select(h,tree,'所属账号').props.onChange('on');h.render();await h.effects();tree=h.render();
assert.equal(table(h,tree).props.pagination.current,1);
select(h,tree,'事件类型').props.onChange('dm');h.render();await h.effects();tree=h.render();
select(h,tree,'接收时间范围').props.onChange(24);h.render();await h.effects();tree=h.render();
const query=new URLSearchParams(h.calls.filter(p=>p.startsWith('/creator/events')).at(-1).split('?')[1]);
assert.equal(query.get('event_type'),'dm');assert.equal(query.get('account_id'),'on');assert.equal(query.get('hours'),'24');assert.equal(query.get('page'),'1');
table(h,tree).props.pagination.onChange(3,50);h.render();await h.effects();tree=h.render();
assert.equal(table(h,tree).props.pagination.current,1);assert.equal(table(h,tree).props.pagination.pageSize,50);
all(tree,node=>node.type===h.antd.Button)[0].props.onClick();h.render();await h.effects();
assert.equal(h.calls.filter(p=>p.startsWith('/creator/events')).length,7);
});
test('event contents distinguish missing data, non-text messages and baseline history',async t=>{
const h=harness();t.after(()=>h.dispose());h.render();await h.effects();const cols=table(h,h.render()).props.columns;
const column=title=>cols.find(c=>c.title===title);
assert.equal(text(column('事件类型').render('comment')),'评论');
assert.equal(text(column('内容').render(null,{message_text:'测试内容',message_type:'text'})),'测试内容');
assert.equal(text(column('内容').render(null,{message_text:'',message_type:'image'})),'图片消息');
assert.equal(text(column('内容').render(null,{message_text:'',message_type:'text'})),'—');
assert.equal(column('发生时间').render(null),'—');assert.equal(column('发生时间').render('date'),'time:date');
assert.equal(column('对应作品').render(''),'—');
const link=column('对应作品').render('123',{id:'event',work_id:'123',work_url:'https://www.douyin.com/video/123',has_work_cover:true});
assert.equal(link.props.href,'https://www.douyin.com/video/123');
const cover=all(link,n=>n.type.displayName==='WorkCover')[0];
assert.equal(cover.props.width,48);assert.equal(cover.props.coverPath,'/creator/events/event/cover');
const missing=column('对应作品').render('123',{id:'event',work_id:'123',work_url:'',has_work_cover:false,work_cover_error:'下载失败'});
assert.equal(missing.props.href,'https://www.douyin.com/video/123');
assert.equal(all(missing,n=>n.type===h.antd.Avatar)[0].props.size,48);
assert.equal(all(missing,n=>n.type===h.antd.Tooltip)[0].props.title,'下载失败');
assert.equal(text(column('来源').render(true)),'同步');assert.equal(text(column('来源').render(false)),'通知');
const user=column('互动用户').render(null,{interactor_uid:'22',interactor_name:'用户',interactor_sec_uid:'MS4wLjABAAAAprofile'});
const profile=all(user,n=>n.type===h.antd.Typography.Link)[0];
assert.equal(profile.props.href,'https://www.douyin.com/user/MS4wLjABAAAAprofile');
assert.equal(profile.props.target,'_blank');assert.equal(text(user),'用户');
const unknown=column('互动用户').render(null,{interactor_uid:'22',interactor_name:'',interactor_sec_uid:''});
assert.equal(all(unknown,n=>n.type===h.antd.Typography.Link).length,0);assert.equal(text(unknown),'昵称未获取');
assert.equal(text(column('账号').render(null,{account_name:'账号',account_uid:'1'})),'账号');
assert.deepEqual(cols.map(c=>c.title),['账号','发生时间','事件类型','互动用户','内容','对应作品','来源']);
assert.equal(cols.some(c=>c.title==='接收时间'||c.dataIndex==='received_at'),false);
});
test('event types and sources have distinct color tags with readable labels',async t=>{
const h=harness();t.after(()=>h.dispose());h.render();await h.effects();
const columns=table(h,h.render()).props.columns;
const type=columns.find(column=>column.title==='事件类型');
const colors=[];
for(const [value,label,color] of [['like','点赞','magenta'],['comment','评论','blue'],['follow','关注','purple'],['repost','转发','orange']]){
const tag=type.render(value);
assert.equal(tag.type,h.antd.Tag);assert.equal(text(tag),label);assert.equal(tag.props.color,color);
colors.push(tag.props.color);
}
const source=columns.find(column=>column.title==='来源');
for(const [baseline,label,color] of [[true,'同步','gold'],[false,'通知','cyan']]){
const tag=source.render(baseline);
assert.equal(tag.type,h.antd.Tag);assert.equal(text(tag),label);assert.equal(tag.props.color,color);
colors.push(tag.props.color);
}
assert.equal(new Set(colors).size,6);
assert.ok(select(h,h.render(),'接收时间范围'));
});
test('long content has a bounded column and displays all text without expansion',async t=>{
const h=harness();t.after(()=>h.dispose());h.render();await h.effects();
const eventTable=table(h,h.render());
assert.equal(eventTable.props.tableLayout,'fixed');
assert.equal(eventTable.props.scroll?.x,undefined);
const content=eventTable.props.columns.find(column=>column.title==='内容');
assert.equal(content.width,'30%');
for(const message of ['长评论内容'.repeat(200),'a'.repeat(2000),`https://example.com/${'path'.repeat(500)}`]){
const paragraph=content.render(null,{message_text:message,message_type:'text'});
assert.equal(paragraph.type,h.antd.Typography.Paragraph);
assert.equal(text(paragraph),message);
assert.equal(paragraph.props.ellipsis,undefined);
}
});
test('read failures are visible and retries do not fake an empty success',async t=>{
let failed=true;const h=harness({get:async path=>{if(failed)throw new Error('服务不可用');return path==='/creator/accounts'?[]:{data:[{id:'1'}],total:1};}});t.after(()=>h.dispose());
h.render();await h.effects();let tree=h.render();
const error=all(tree,n=>n.type===h.antd.Alert)[0];assert.equal(error.props.title,'事件读取失败');assert.equal(error.props.description,'服务不可用');
failed=false;all(tree,n=>n.type===h.antd.Button)[0].props.onClick();h.render();await h.effects();tree=h.render();
assert.equal(all(tree,n=>n.type===h.antd.Alert).length,0);assert.equal(table(h,tree).props.dataSource.length,1);
});
test('a late response for previous filters cannot overwrite current events',async t=>{
let resolveOld;
const h=harness({get:path=>{if(path==='/creator/accounts')return Promise.resolve([]);if(path.includes('event_type=comment'))return Promise.resolve({data:[{id:'current'}],total:1});return new Promise(resolve=>{resolveOld=resolve;});}});t.after(()=>h.dispose());
let tree=h.render();await h.effects();select(h,tree,'事件类型').props.onChange('comment');h.render();await h.effects();
resolveOld({data:[{id:'old'}],total:99});await h.effects();tree=h.render();
assert.equal(table(h,tree).props.dataSource[0].id,'current');assert.equal(table(h,tree).props.pagination.total,1);
});