2026-10-18 00:12:50 +08:00
const { chromium } = require ( process . env . PLAYWRIGHT_MODULE || 'playwright' );
const fs = require ( 'fs' ); const path = require ( 'path' );
const adapter = path . resolve ( __dirname , '..' );
const web = process . env . PIGWAY_WEB_ROOT || path . resolve ( adapter , '../pigway-web-manager/web' );
const output = adapter + '/.data/audit-validation' ; fs . mkdirSync ( output ,{ recursive : true });
( async ()=>{ const browser = await chromium . launch ({ headless : true }); const page = await browser . newPage (); let errors = []; page . on ( 'pageerror' , e => errors . push ( e . message ));
await page . route ( 'http://audit.local/**' , async route =>{ const u = new URL ( route . request (). url ()); let body , type = 'text/javascript' ;
if ( u . pathname === '/' ){ type = 'text/html' ; body = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="/assets/css/base.css"><link rel="stylesheet" href="/assets/css/layout.css"><link rel="stylesheet" href="/assets/css/controls.css"><link rel="stylesheet" href="/plugin/styles.css"><main id="cards"></main><script type="module">import {agentHostCard} from '/plugin/overview.js';import {state} from '/assets/js/state.js';window.render=(lang,plugins={})=>{state.LANG=lang;document.querySelector('#cards').innerHTML=agentHostCard({host:{id:'gateway',name:'Gateway'},online:true,status:{version:'3.11.2',machine:{id:'gateway',name:'家庭网关 100%',hostname:'gateway'},device:{model:'Raspberry Pi 4',os:'Debian 13',architecture:'aarch64',cpu_count:4},system:{cpu_percent:12,memory_percent:25,temperature_c:42,cpu_freq_mhz:1500,memory_used_bytes:1000000,memory_total_bytes:4000000,memory_available_bytes:3000000,storage_inventory:[]},network:{interface:'eth0',kind:'ETH',link_speed_mbps:1000,link:true,ip_label:'IP4',ip:'192.168.1.2',rx_bytes_per_second:10,tx_bytes_per_second:20},display:{owner:'DISK_CRITICAL',alerts:[{id:'DISK_CRITICAL'}]},plugins,watches:[]}})};render('zh');</script>` ;}
else if ( u . pathname === '/assets/js/state.js' ) body = 'export const state={LANG:"zh"};' ;
else if ( u . pathname === '/assets/js/i18n.js' ) body = 'export const tr=x=>x;' ;
else if ( u . pathname === '/assets/js/overview.js' ){ let source = fs . readFileSync ( web + '/js/overview.js' , 'utf8' ); body = 'import {state as appState} from "/assets/js/state.js";\n' + source . slice ( source . indexOf ( 'export function bytes' ), source . indexOf ( 'export const connectionLabels' )); }
else if ( u . pathname === '/plugin/hardware.js' ) body = 'export const fanMetric=()=>"";export const pluginSummary=()=>"";' ;
else { let file = u . pathname . startsWith ( '/plugin/' ) ? adapter + '/plugin/ui/' + u . pathname . slice ( 8 ) : web + '/' + u . pathname . replace ( '/assets/' , '' ); if ( ! fs . existsSync ( file )) throw Error ( file ); body = fs . readFileSync ( file ); if ( file . endsWith ( '.css' )) type = 'text/css' ;}
await route . fulfill ({ status : 200 , contentType : type , body });});
await page . goto ( 'http://audit.local/' ); await page . waitForFunction (()=> typeof window . render === 'function' );
for ( const [ name , width , height ] of [[ 'desktop' , 1440 , 1000 ],[ 'tablet' , 768 , 1024 ],[ 'mobile' , 390 , 844 ]]){ await page . setViewportSize ({ width , height }); for ( const lang of [ 'zh' , 'en' ]){ await page . evaluate ( lang => render ( lang ), lang ); await page . waitForTimeout ( 100 ); const text = await page . locator ( 'body' ). innerText (); if ( ! text . includes ( lang === 'zh' ? '主要告警' : 'Primary alert' )) throw Error ( 'Missing label' ); if ( text . includes ( '显示状态' )) throw Error ( 'Old label' ); if ( await page . evaluate (()=> document . documentElement . scrollWidth > innerWidth )) throw Error ( 'Overflow ' + name ); await page . screenshot ({ path : output + '/' + name + '-' + lang + '.png' , fullPage : true });}}
const hardware = { hat : { name : 'Yahboom RGB Cooling HAT' , online : true , capabilities : { display : true , rgb : true }, status : { available : true , oled_page : 'NORMAL_HOME' , rgb_mode : 'OFF' }}};
for ( const width of [ 1440 , 768 , 390 ]){
await page . setViewportSize ({ width , height : 1000 });
await page . evaluate ( p => render ( 'zh' , p ), hardware );
const primary = page . locator ( '[data-refresh-group="display"]' );
if (( await primary . locator ( '.value' ). innerText ()) !== 'OLED 正常主页 · RGB 全灭' ) throw Error ( 'Merged OLED/RGB text' );
2026-10-18 13:32:51 +08:00
if ( await primary . locator ( '.pi-alert-heading .semantic-status' ). count ()) throw Error ( 'Healthy hardware must not show redundant status' );
2026-10-18 00:12:50 +08:00
if ( await page . locator ( '[data-manage-plugin-host]' ). count ()) throw Error ( 'Overview must not expose plugin management' );
if ( await page . locator ( '.group.action-row' ). count ()) throw Error ( 'Standalone hardware row remains' );
if ( ! ( await primary . innerText ()). includes ( 'Yahboom RGB Cooling HAT' )) throw Error ( 'Hardware name missing' );
if ( await page . evaluate (()=> document . documentElement . scrollWidth > innerWidth )) throw Error ( 'Merged display overflow' );
await page . screenshot ({ path : output + '/merged-' + width + '.png' , fullPage : true });
}
hardware . hat . online = false ;
await page . evaluate ( p => render ( 'zh' , p ), hardware );
if (( await page . locator ( '.pi-alert-heading .semantic-status' ). innerText ()) !== '离线' ) throw Error ( 'Offline hardware shown online' );
2026-10-18 13:32:51 +08:00
if ( ! ( await page . locator ( '.pi-alert-heading' ). evaluate ( n => Math . abs ( n . lastElementChild . getBoundingClientRect (). right - n . getBoundingClientRect (). right ) < 1 ))) throw Error ( 'Hardware issue status is not right aligned' );
2026-10-18 00:12:50 +08:00
if (( await page . locator ( '[data-refresh-group="display"] .value' ). innerText ()). includes ( 'RGB 全灭' )) throw Error ( 'Stale RGB shown as current' );
await page . evaluate (()=> render ( 'zh' ,{}));
if (( await page . locator ( '.pi-alert-heading .semantic-status' ). innerText ()) !== '未接入' ) throw Error ( 'Absent hardware shown online' );
if ( errors . length ) throw Error ( errors . join ( '\n' )); console . log ( 'PASS: merged hardware display; online/offline/absent state; plugin management removed from overview; desktop/tablet/mobile; Chinese/English; no overflow or browser errors' ); await browser . close ();})(). catch ( e =>{ console . error ( e ); process . exit ( 1 )});