Files
kefu/deploy/review-visibility-20260916/frontend-test.mjs
T
2026-09-21 10:34:06 +08:00

117 lines
10 KiB
JavaScript

import {createRequire} from 'node:module';
import {mkdirSync,writeFileSync} from 'node:fs';
import assert from 'node:assert/strict';
const require=createRequire('C:/kefu/admin-web/package.json');
const {chromium}=require('playwright');
const folder='C:/kefu/deploy/review-visibility-20260916';mkdirSync(`${folder}/qa`,{recursive:true});
const browser=await chromium.launch({executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe',headless:true});
const results=[],errors=[];
const task={key:'test-review-001',name:'模拟客户',sessionId:'test-session',messages:[{time:'10:00',text:'合成测试问题'},{time:'10:01',text:'补充测试问题'}],answer:'合成回复草稿\n请确认后发送。',awaitingReview:true,canApprove:true,needsHuman:false,isProtocol:true,reviewReason:'测试命中选择性审核规则',action:'等待人工审核',stepIndex:4};
for(const [label,root] of [['protocol','C:/wechat_rpa'],['classic','C:/kefu/wechat_rpa']]){
const context=await browser.newContext({viewport:{width:1440,height:900},deviceScaleFactor:2});
await context.route('**/*',route=>route.request().url().startsWith('file:')?route.continue():route.abort());
async function page(view='queue',extra={}){
const p=await context.newPage();p.on('pageerror',e=>errors.push({label,error:e.message}));
await p.goto(`file:///${root}/assets/ui/zhenyang-ai-console.html`);
await p.waitForTimeout(250);
await p.evaluate(state=>{
window.calls=[];window.qtBridge=new Proxy({}, {get:(_,method)=>(...args)=>calls.push({method,args})});
applyState(state);
},{view,demo:false,ready:true,metrics:{review:2,human:1},people:[{key:task.key,name:task.name,status:'待审核',bubbles:2}],selectedKey:task.key,task,queueFilter:'全部',system:{scale:'自动'},...extra});
return p;
}
async function check(name,fn){try{await fn();results.push({label,name,passed:true});}catch(e){results.push({label,name,passed:false,error:String(e)});}}
await check('任务页显示审核原因、完整消息、草稿、按钮和计数',async()=>{
const p=await page();assert.equal(await p.locator('.queue-task-detail .message-line').count(),2);
assert.match(await p.locator('.queue-task-detail').innerText(),/测试命中选择性审核规则/);
assert.match(await p.locator('.queue-task-detail .answer-block').innerText(),/合成回复草稿/);
assert.equal(await p.locator('[data-action="approve-task"]').count(),1);
assert.equal(await p.locator('[data-live-metric="review"]').innerText(),'2');
await p.screenshot({path:`${folder}/qa/${label}-review-queue.png`,scale:'css',fullPage:true});await p.close();
});
await check('审批按钮只把所选任务key交给模拟桥接',async()=>{
const p=await page();await p.locator('[data-action="approve-task"]').click();
assert.deepEqual(await p.evaluate(()=>calls.filter(c=>c.method==='approveTask')),[{method:'approveTask',args:[task.key]}]);await p.close();
});
await check('协议草稿提示不声称填入企微输入框',async()=>{
const p=await page();const text=await p.locator('.task-review-hint').innerText();assert.match(text,/协议回复草稿已保存在本应用/);assert.doesNotMatch(text,/输入框|回车/);await p.close();
});
await check('视觉审核自动页有可操作入口和准确提示',async()=>{
const p=await page('auto',{task:{...task,isProtocol:false}});assert.equal(await p.locator('[data-action="approve-task"]').count(),1);
assert.match(await p.locator('.task-review-hint').innerText(),/企业微信中确认草稿/);await p.close();
});
await check('媒体无草稿需人工处理不会出现发送或重试',async()=>{
const p=await page('queue',{task:{...task,answer:'',canApprove:false,needsHuman:true,reviewReason:'图片消息需要人工查看'},people:[{key:task.key,name:task.name,status:'需人工'}]});
assert.equal(await p.locator('[data-action="approve-task"]').count(),0);assert.equal(await p.locator('[data-action="retry-task"]').count(),0);
assert.match(await p.locator('.task-review-hint').innerText(),/图片消息需要人工查看/);
assert.equal(await p.locator('.human-pending').count(),1);await p.close();
});
await check('安全阻断优先于有草稿审批flag',async()=>{
const p=await page('queue',{task:{...task,needsHuman:true}});assert.equal(await p.locator('[data-action="approve-task"]').count(),0);await p.close();
});
await check('发送结果不确定只允许核对后人工处理',async()=>{
const p=await page('queue',{task:{...task,awaitingReview:false,needsHuman:true,canRetry:true,deliveryUncertain:true,reviewReason:'发送结果待人工核对'}});
assert.equal(await p.locator('[data-action="approve-task"]').count(),0);assert.equal(await p.locator('[data-action="retry-task"]').count(),0);
assert.match(await p.locator('[data-action="handoff-task"]').innerText(),/核对后转人工/);assert.match(await p.locator('.task-review-hint').innerText(),/先在企业微信核对是否已发出/);await p.screenshot({path:`${folder}/qa/${label}-uncertain-delivery.png`,scale:'css',fullPage:true});await p.close();
});
await check('未知回执在自动页与队列页只能核对后转人工',async()=>{
for(const view of ['auto','queue']){
const p=await page(view,{task:{...task,awaitingReview:false,needsHuman:true,canRetry:true,deliveryUncertain:true,reviewReason:'发送结果待人工核对'}});
assert.equal(await p.locator('[data-action="approve-task"],[data-action="retry-task"],[data-action="cancel-task"]').count(),0);
assert.equal(await p.locator('[data-action="handoff-task"]').count(),1);
assert.match(await p.locator('[data-action="handoff-task"]').innerText(),/核对后转人工/);await p.close();
}
});
await check('可重试生成失败任务提供重试并传递正确key',async()=>{
const p=await page('queue',{task:{...task,awaitingReview:false,needsHuman:true,canRetry:true,answer:'',reviewReason:'模型请求失败'}});
assert.equal(await p.locator('[data-action="approve-task"]').count(),0);await p.locator('[data-action="retry-task"]').click();
assert.deepEqual(await p.evaluate(()=>calls.filter(c=>c.method==='retryTask')),[{method:'retryTask',args:[task.key]}]);await p.close();
});
await check('未知审批权限或纯空白回复不开放发送',async()=>{
for(const draft of [{...task,canApprove:undefined},{...task,answer:' \n '},{...task,key:''}]){
const p=await page('queue',{task:draft});assert.equal(await p.locator('[data-action="approve-task"]').count(),0);await p.close();
}
});
await check('审核原因单独变化会刷新界面',async()=>{
const p=await page();await p.evaluate(task=>applyState({view:'queue',task:{...task,reviewReason:'新的风险原因'}}),task);
assert.match(await p.locator('.task-review-hint').innerText(),/新的风险原因/);await p.close();
});
await check('同一回复进入与离开审核会刷新按钮',async()=>{
const p=await page('queue',{task:{...task,awaitingReview:false}});assert.equal(await p.locator('[data-action="approve-task"]').count(),0);
await p.evaluate(task=>applyState({view:'queue',task}),task);assert.equal(await p.locator('[data-action="approve-task"]').count(),1);
await p.evaluate(task=>applyState({view:'queue',task:{...task,awaitingReview:false}}),task);assert.equal(await p.locator('[data-action="approve-task"]').count(),0);await p.close();
});
await check('待审核与需人工筛选调用中文状态key',async()=>{
const p=await page();for(const filter of ['待审核','需人工'])await p.locator(`[data-action="queue-filter"][data-key="${filter}"]`).click();
assert.deepEqual(await p.evaluate(()=>calls.filter(c=>c.method==='setQueueFilter').map(c=>c.args[0])),['待审核','需人工']);await p.close();
});
await check('自动页提醒可直达任务页和人工筛选',async()=>{
const p=await page('auto');await p.locator('[data-action="open-review-queue"][data-key="需人工"]').click();
assert.match(await p.locator('h1').innerText(),/任务队列/);
const got=await p.evaluate(()=>calls.filter(c=>['navigate','setQueueFilter'].includes(c.method)));
assert.deepEqual(got,[{method:'navigate',args:['queue']},{method:'setQueueFilter',args:['需人工']}]);await p.close();
});
await check('断开企微时仍可查看、取消和转人工',async()=>{
const p=await page('queue',{wecomConnected:false,task:{...task,canApprove:false,needsHuman:true}});
await p.locator('[data-action="cancel-task"]').click();await p.locator('[data-action="handoff-task"]').click();
assert.deepEqual(await p.evaluate(()=>calls.filter(c=>['cancelTask','handoffTask'].includes(c.method))),[{method:'cancelTask',args:[task.key]},{method:'handoffTask',args:[task.key]}]);await p.close();
});
await check('审核原因及回复内容转义HTML',async()=>{
const value='<img src=x onerror="window.__injected=1">';const p=await page('queue',{task:{...task,reviewReason:value,answer:value}});
assert.equal(await p.locator('.task-review-hint img,.answer-block img').count(),0);assert.equal(await p.evaluate(()=>window.__injected),undefined);await p.close();
});
await check('审核数从零到非零改变提醒样式',async()=>{
const p=await page('queue',{metrics:{review:0,human:0}});assert.equal(await p.locator('.has-pending').count(),0);
await p.evaluate(()=>applyState({view:'queue',metrics:{review:3,human:1}}));assert.equal(await p.locator('.has-pending').count(),2);assert.equal(await p.locator('[data-live-metric="review"]').innerText(),'3');await p.close();
});
if(label==='protocol')await check('窄窗口审核操作仍可见且不横向溢出',async()=>{
const p=await page();await p.setViewportSize({width:640,height:640});await p.waitForTimeout(100);
const button=p.locator('[data-action="approve-task"]');await button.scrollIntoViewIfNeeded();assert.equal(await button.isVisible(),true);
const overflow=await p.evaluate(()=>{const m=document.getElementById('main');return m.scrollWidth-m.clientWidth});assert.ok(overflow<=2,`overflow ${overflow}`);
await p.screenshot({path:`${folder}/qa/${label}-review-narrow.png`,scale:'css'});await p.close();
});
await context.close();
}
await browser.close();const report={results,errors,total:results.length,passed:results.filter(r=>r.passed).length};
writeFileSync(`${folder}/qa/frontend-results.json`,JSON.stringify(report,null,2));console.log(JSON.stringify(report));if(results.some(r=>!r.passed)||errors.length)process.exitCode=1;