112 lines
8.3 KiB
JavaScript
112 lines
8.3 KiB
JavaScript
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='<img src=x onerror="window.__escaped=false"> & "助理"';
|
|
const longId='M:'+'0123456789'.repeat(20)+'<svg onload="window.__escaped=false">';
|
|
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;});
|