const assert=require('node:assert/strict'); const fs=require('node:fs'); const path=require('node:path'); const {chromium}=require('C:/kefu/admin-web/node_modules/playwright'); const out=__dirname; const customTag=String(process.env.REVIEW_PANEL_TAG||'custom').replace(/[^a-zA-Z0-9_-]/g,'_'); const targets=process.env.REVIEW_PANEL_ROOT ? [[customTag,process.env.REVIEW_PANEL_ROOT.replace(/\\/g,'/')]] : [['production','C:/wechat_rpa'],['source','C:/kefu/wechat_rpa']]; const reportName=process.env.REVIEW_PANEL_ROOT ? `panel-dom-results-${customTag}.json` : 'panel-dom-results.json'; const results=[]; let checks=0; function check(value,message){assert(value,message);checks++;} (async()=>{ const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files/Google/Chrome/Application/chrome.exe'}); try{ for(const [tag,root] of targets){ for(const scale of [1,2]){ const context=await browser.newContext({viewport:scale===1?{width:1280,height:1000}:{width:960,height:850},deviceScaleFactor:scale}); await context.route(/^https?:/,route=>route.abort()); await context.addInitScript(()=>{ window.bridgeCalls=[]; window.qtBridge={ refreshReviewAssistant:()=>window.bridgeCalls.push(['refresh']), chooseReviewAssistant:()=>window.bridgeCalls.push(['choose']), setReviewAssistantEnabled:value=>window.bridgeCalls.push(['toggle',value]), }; }); const page=await context.newPage();const errors=[]; page.on('pageerror',error=>errors.push(String(error))); await page.goto('file:///'+root+'/assets/ui/zhenyang-ai-console.html'); const draw=async(recipient,overrides={})=>{ await page.evaluate(({recipient,overrides})=>{ const settings={supported:true,ready:true,enabled:false,busy:false, accountId:'168800000000001',accountName:'合成测试托管账号',recipient, preview:'示例内容,仅用于离线界面检查。',...overrides}; window.applyState({view:'automation',demo:false,ready:true,automation:{transport:'visual',reviewAssistant:settings}}); render('automation');document.documentElement.classList.add('embedded');window.fit(); },{recipient,overrides}); await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); }; const fullId='M:168885000012345678901234567890'; for(const [kind,recipient] of [ ['named',{id:fullId,name:'微信联系人',displayName:'陈医生(合成测试)',nameKnown:true,label:'私人微信'}], ['unknown',{id:fullId,name:'微信联系人',displayName:'微信联系人',nameKnown:false,label:'私人微信'}], ]){ await draw(recipient); const card=page.locator('.assistant-recipient-card'); check(await card.getAttribute('aria-label')==='已配置收件人',tag+' configured recipient label'); check(await card.locator('.assistant-recipient-id').innerText()===fullId,tag+' full ID rendered'); const expected=kind==='named'?'陈医生(合成测试)':'昵称未读取,请核对联系编号'; check(await card.locator('.assistant-recipient-name').innerText()===expected,tag+' nickname is explicit'); const metrics=await card.evaluate(el=>{ const id=el.querySelector('.assistant-recipient-id'),name=el.querySelector('.assistant-recipient-name'); const b=el.getBoundingClientRect(),n=name.getBoundingClientRect(),i=id.getBoundingClientRect(); return {card:{left:b.left,right:b.right,width:b.width},nameFont:getComputedStyle(name).fontSize, idFont:getComputedStyle(id).fontSize,nameInside:n.left>=b.left&&n.right<=b.right, idInside:i.left>=b.left&&i.right<=b.right,scrollWidth:el.scrollWidth,clientWidth:el.clientWidth, whiteSpace:getComputedStyle(id).whiteSpace,overflowWrap:getComputedStyle(id).overflowWrap}; }); check(metrics.nameInside&&metrics.idInside&&metrics.scrollWidth<=metrics.clientWidth+1,tag+' card has no clipping'); check(parseFloat(metrics.nameFont)>parseFloat(metrics.idFont),tag+' nickname has stronger hierarchy'); check(metrics.whiteSpace==='normal'&&metrics.overflowWrap==='anywhere',tag+' ID can wrap'); check(await page.locator('[data-action=review-assistant-toggle]').getAttribute('aria-checked')==='false',tag+' enabled state unchanged'); const screenshot=path.join(out,`recipient-${tag}-${kind}-${scale}x.png`); await card.locator('..').screenshot({path:screenshot}); results.push({tag,scale,kind,screenshot,metrics}); } // Legacy objects never turn a generic contact placeholder into an apparent name. await draw({id:fullId,name:'微信联系人'}); check((await page.locator('.assistant-recipient-name').innerText()).includes('昵称未读取'),tag+' legacy generic name is unknown'); await draw({id:fullId,name:'旧字段真实姓名'}); check(await page.locator('.assistant-recipient-name').innerText()==='旧字段真实姓名',tag+' legacy genuine name remains visible'); await draw({id:fullId,displayName:'陈医生',nameKnown:false}); check((await page.locator('.assistant-recipient-name').innerText()).includes('昵称未读取'),tag+' explicit unknown wins'); await draw({}); check(await page.locator('.assistant-recipient-card').getAttribute('aria-label')==='未配置收件人',tag+' empty selection distinct'); check(await page.locator('[data-action=review-assistant-toggle]').isDisabled(),tag+' empty selection cannot enable'); // Use literal hostile strings as data only; neither elements nor attributes may be created. const hostile=' & "助理"'; const longId='M:'+'0123456789'.repeat(20)+''; await page.evaluate(()=>window.__escaped=true); await draw({id:longId,displayName:hostile,nameKnown:true,label:hostile}); check(await page.locator('.assistant-recipient-name').textContent()===hostile,tag+' name escaped as exact text'); check(await page.locator('.assistant-recipient-id').textContent()===longId,tag+' full hostile ID escaped'); check(await page.locator('.assistant-recipient-card img,.assistant-recipient-card [onerror],.assistant-recipient-card [onload]').count()===0,tag+' injected markup absent'); check(await page.evaluate(()=>window.__escaped===true),tag+' no injected handlers executed'); check(await page.locator('.assistant-recipient-card').evaluate(el=>el.scrollWidth<=el.clientWidth+1),tag+' long ID wraps without horizontal overflow'); await draw({id:fullId,name:'合成助理'},{enabled:true,busy:true}); check(await page.locator('[data-action=review-assistant-toggle]').getAttribute('aria-checked')==='true',tag+' enabled display preserved'); check(await page.locator('[data-action=review-assistant-toggle]').isDisabled(),tag+' busy disables toggle'); check(await page.locator('[data-action=review-assistant-choose]').isDisabled(),tag+' busy disables picker'); await draw({id:fullId,name:'合成助理'},{ready:false,reason:'合成不可用原因'}); check(await page.locator('[data-action=review-assistant-choose]').isDisabled(),tag+' unavailable picker unchanged'); check(await page.locator('.assistant-recipient-card').locator('..').innerText().then(t=>t.includes('合成不可用原因')),tag+' availability reason retained'); await draw({id:fullId,name:'合成助理'}); await page.evaluate(()=>window.bridgeCalls=[]); await page.locator('[data-action=review-assistant-choose]').click(); await page.locator('[data-action=review-assistant-refresh]').click(); await page.locator('[data-action=review-assistant-toggle]').click(); const calls=await page.evaluate(()=>window.bridgeCalls); check(calls.some(call=>call[0]==='choose')&&calls.some(call=>call[0]==='refresh'),tag+' existing picker/refresh bridge names unchanged'); const toggle=calls.find(call=>call[0]==='toggle'); check(toggle&&JSON.parse(toggle[1]).expected_account==='168800000000001'&&JSON.parse(toggle[1]).enabled===true,tag+' toggle account guard unchanged'); check(errors.length===0,tag+' no browser script errors: '+errors.join(';')); await context.close(); } } }finally{await browser.close();} fs.writeFileSync(path.join(out,reportName),JSON.stringify({status:'passed',checks,screenshots:results.length,results},null,2)); console.log(JSON.stringify({checks,screenshots:results.length,status:'passed'})); })().catch(error=>{console.error(error);process.exitCode=1;});