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=[],streams=[]; 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};},creatorSubscribe:(path,onMessage,signal,onStatus)=>{ streams.push({path,onMessage,signal,onStatus});onStatus?.('connected'); return new Promise(resolve=>signal.addEventListener('abort',resolve,{once:true})); }}; return require(id); }; loaded._compile(transpile(file),file); return{antd,calls,streams,push(){streams.at(-1).onMessage('data: {}');},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.ok(h.calls.filter(p=>p.startsWith('/creator/events')).length>=7); assert.ok(h.streams.every(s=>s.path==='/creator/updates')); const before=h.calls.length;h.push();await h.effects();assert.ok(h.calls.length>before); assert.equal(table(h,h.render()).props.pagination.current,1); }); test('stream disconnect is visible and cleanup aborts the subscription',async t=>{ const h=harness();t.after(()=>h.dispose());h.render();await h.effects(); h.streams.at(-1).onStatus('disconnected'); assert.ok(all(h.render(),n=>n.type===h.antd.Alert&&n.props.title==='实时更新连接已断开').length); const signal=h.streams.at(-1).signal;h.dispose();assert.equal(signal.aborted,true); }); 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); });