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.match(text(user),/用户/);assert.match(text(user),/UID:22/); 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.match(text(unknown),/昵称未获取/); assert.equal(text(column('所属账号').render(null,{account_name:'账号',account_uid:'1'})),'账号'); assert.deepEqual(cols.slice(0,2).map(c=>c.title),['发生时间','接收时间']); }); 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); });