import {createRequire} from 'node:module'; import {writeFileSync,mkdirSync} from 'node:fs'; const require=createRequire('C:/kefu/admin-web/package.json');const{chromium}=require('playwright'); const folder='C:/kefu/deploy/ui-performance-20260916';mkdirSync(`${folder}/qa`,{recursive:true}); const before=process.argv.includes('--before'),root=before?'C:/wechat_rpa':`${folder}/payload`; const browser=await chromium.launch({executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe',headless:true}); const context=await browser.newContext({viewport:{width:1280,height:720},deviceScaleFactor:2}); await context.route('**/*',route=>route.request().url().startsWith('file:')?route.continue():route.abort()); const errors=[],results=[]; async function page(view){ const p=await context.newPage();p.on('pageerror',e=>errors.push(e.message)); await p.goto(`file:///${root}/assets/ui/zhenyang-ai-console.html`);await p.waitForTimeout(430); await p.evaluate(view=>{ window.calls=[]; window.qtBridge=new Proxy({}, {get:(_,method)=>(...args)=>{ calls.push({method,args:args.filter(a=>typeof a!=='function')}); const callback=args.at(-1);if(typeof callback==='function')callback(JSON.stringify({ok:true,message:'测试已保存'})); }}); applyState({view,demo:false,ai:{name:'测试客服'},automation:{poll:2,batch:20,delay:1,idle:5,context:8},system:{scale:'自动',retention:90,serverUrl:'http://test.invalid'}}); },view);return p; } async function check(name,fn){let value;try{value=await fn();results.push({name,passed:!!value});}catch(e){results.push({name,passed:false,error:String(e)});}} try{ await check('编辑内容在后台刷新、失焦后保留',async()=>{ const p=await page('ai');await p.locator('#ai-name').fill('尚未保存的新名字'); await p.evaluate(()=>{document.activeElement.blur();applyState({view:'ai',wecomConnected:true,ai:{name:'测试客服'}})}); const ok=await p.locator('#ai-name').inputValue()==='尚未保存的新名字';await p.close();return ok; }); await check('切页返回仍保留未保存草稿',async()=>{ const p=await page('ai');await p.locator('#ai-name').fill('跨页面草稿'); await p.locator('[data-view="system"]').click();await p.locator('[data-view="ai"]').click(); const ok=await p.locator('#ai-name').inputValue()==='跨页面草稿';await p.close();return ok; }); await check('快速连续导航只提交最后一页',async()=>{ const p=await page('auto');await p.evaluate(()=>{for(const name of ['ai','logs','system'])document.querySelector(`[data-view="${name}"]`).click()}); await p.waitForTimeout(120);const calls=await p.evaluate(()=>window.calls.filter(c=>c.method==='navigate')); const ok=calls.length===1&&calls[0].args[0]==='system';await p.close();return ok; }); await check('旧快照不会把新页面切回去',async()=>{ const p=await page('ai');await p.locator('[data-view="system"]').click(); await p.evaluate(()=>applyState({view:'ai',ai:{name:'旧状态'}}));const ok=await p.locator('h1').innerText()==='系统设置';await p.close();return ok; }); await check('切页前提交编辑快照,不误保存旧值',async()=>{ const p=await page('automation');await p.locator('#auto-poll').fill('7');await p.locator('[data-view="system"]').click();await p.waitForTimeout(500); const values=await p.evaluate(()=>calls.filter(c=>c.method==='saveAutomationSettings').map(c=>JSON.parse(c.args[0]).poll)); await p.close();return values.length===1&&values[0]===7; }); await check('缩放下拉选择只保存一次',async()=>{ const p=await page('system');await p.locator('#sys-scale').selectOption('200%');await p.waitForTimeout(420); const n=await p.evaluate(()=>calls.filter(c=>c.method==='setSystemField').length);await p.close();return n===1; }); await check('手动保存会消费待执行的自动保存',async()=>{ const p=await page('system');await p.locator('#sys-retention').fill('123');await p.locator('[data-action="save-system"]').click();await p.waitForTimeout(400); const values=await p.evaluate(()=>calls.filter(c=>['setSystemField','saveSystemSettings'].includes(c.method)));await p.close();return values.length===1; }); await check('中文输入组合期间不提交搜索',async()=>{ const p=await page('logs');await p.locator('#event-search').evaluate(e=>{ e.dispatchEvent(new CompositionEvent('compositionstart',{bubbles:true}));e.value='测试';e.dispatchEvent(new InputEvent('input',{bubbles:true,isComposing:true})); });await p.waitForTimeout(350);const early=await p.evaluate(()=>calls.filter(c=>c.method==='setEventQuery').length); await p.locator('#event-search').evaluate(e=>e.dispatchEvent(new CompositionEvent('compositionend',{bubbles:true})));await p.waitForTimeout(350); const count=await p.evaluate(()=>calls.filter(c=>c.method==='setEventQuery').length);await p.close();return early===0&&count===1; }); await check('编辑时 100 次后台状态更新保持输入节点和光标',async()=>{ const p=await page('ai');await p.locator('#ai-name').fill('编辑中'); const ok=await p.evaluate(()=>{const input=document.getElementById('ai-name');input.setSelectionRange(1,2);for(let i=0;i<100;i++)applyState({view:'ai',runtime:String(i),wecomConnected:!!(i%2)});return input===document.getElementById('ai-name')&&input.value==='编辑中'&&input.selectionStart===1&&input.selectionEnd===2});await p.close();return ok; }); await check('保存确认后接收后续云端修改',async()=>{ const p=await page('ai');await p.locator('#ai-name').fill('已保存'); await p.evaluate(()=>{document.activeElement.blur();applyState({view:'ai',ai:{name:'已保存'}});applyState({view:'ai',ai:{name:'后续云端名字'}})}); const ok=await p.locator('#ai-name').inputValue()==='后续云端名字';await p.close();return ok; }); await check('搜索结果刷新保留正在输入的搜索框和光标',async()=>{ const p=await page('wechatdb');await p.locator('#wechat-db-search').fill('模拟查询'); const ok=await p.evaluate(()=>{const input=document.getElementById('wechat-db-search');input.setSelectionRange(2,2); applyState({view:'wechatdb',wechatDb:{query:'模拟查询',loading:false,refreshedAt:'刚刚',accountCount:1, accounts:[{id:'a',name:'命中账号',databaseCount:1,messageCount:10,conversationCount:1}],conversations:[],messages:[],files:[]}}); return document.getElementById('wechat-db-search')===input&&document.activeElement===input&&input.selectionStart===2&&document.body.textContent.includes('命中账号'); });await p.close();return ok; }); const p=await page('archive'); const benchmark=await p.evaluate(()=>{ const html=('
普通内容 ✦ ◇ 🛡 💊 Ⅱ
').repeat(10000),times=[]; for(let i=0;i<12;i++){const t=performance.now();polishedIcons(html);times.push(performance.now()-t);} const archives=Array.from({length:500},(_,i)=>({key:String(i),name:`模拟客户 ${i}`,avatar:'客',preview:'模拟历史会话',count:20,time:'10:00',badge:'已归档'})); const t=performance.now();applyState({view:'archive',archives}); return {iconMedianMs:times.sort((a,b)=>a-b)[Math.floor(times.length/2)],archive500Ms:performance.now()-t}; });await p.screenshot({path:`${folder}/qa/${before?'before':'after'}-archive.png`,scale:'css'});await p.close(); const report={before,results,errors,benchmark};writeFileSync(`${folder}/qa/${before?'before':'after'}.json`,JSON.stringify(report,null,2));console.log(JSON.stringify(report)); if(!before&&(results.some(r=>!r.passed)||errors.length))process.exitCode=1; }finally{await browser.close();}