Files
awoooi/apps/web/scripts/verify-frontend.js
OG T 1c66a05335 feat(qa): add Playwright frontend visual verification script
- Create apps/web/scripts/verify-frontend.js (無頭偵察兵)
- Detects Console errors (zero-error policy)
- Verifies DOM content (INC-*, RPS, Latency)
- Takes full-page screenshot for evidence
- Update SRE Skill with mandatory verification step

Constitutional Law 14/15: No curl-only verification allowed!

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-23 01:19:08 +08:00

168 lines
5.7 KiB
JavaScript

/**
* AWOOOI 無頭偵察兵 - Playwright 前端視覺驗證
* ==================================================
*
* 職責:
* 1. 驅動真實瀏覽器訪問正式環境
* 2. 擷取 Console 錯誤 (紅字檢測)
* 3. 檢查關鍵 DOM 元素是否渲染真實數據
* 4. 等待 SSE 串流與 React Hydration
*
* 用法:
* node scripts/verify-frontend.js
*
* 憲法條款 14: 嚴禁依賴 curl 盲目驗收
* 憲法條款 15: 必須親眼確認 DOM 渲染結果
*/
const { chromium } = require('playwright');
const TARGET_URL = process.env.TARGET_URL || 'https://awoooi.wooo.work';
const WAIT_TIME = parseInt(process.env.WAIT_TIME || '8000', 10);
async function runQA() {
console.log('============================================================');
console.log(' AWOOOI 無頭偵察兵 - Playwright 視覺驗證');
console.log('============================================================');
console.log(`目標: ${TARGET_URL}`);
console.log(`等待時間: ${WAIT_TIME}ms`);
console.log('');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'AWOOOI-QA-Bot/1.0 Playwright',
});
const page = await context.newPage();
// 收集 Console 錯誤
const errors = [];
const warnings = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
} else if (msg.type() === 'warning') {
warnings.push(msg.text());
}
});
page.on('pageerror', (err) => {
errors.push(`[PageError] ${err.message}`);
});
// Request 失敗追蹤
page.on('requestfailed', (request) => {
errors.push(`[RequestFailed] ${request.url()} - ${request.failure()?.errorText}`);
});
try {
console.log('🚀 導航至目標頁面...');
await page.goto(TARGET_URL, {
waitUntil: 'domcontentloaded',
timeout: 60000,
});
console.log(`⏳ 等待 ${WAIT_TIME}ms (SSE 串流 + React Hydration)...`);
await page.waitForTimeout(WAIT_TIME);
// =========================================================================
// DOM 內容檢查
// =========================================================================
console.log('\n📋 DOM 內容分析...');
const bodyText = await page.textContent('body');
const pageTitle = await page.title();
// 關鍵指標檢查
const checks = {
// 事件數據檢查
hasIncidentId: bodyText.includes('INC-'),
hasAlertKeywords: bodyText.includes('告警') || bodyText.includes('警告') || bodyText.includes('事件'),
// GlobalPulse 指標檢查 (非零數據)
hasRpsData: /\d+\.\d+\s*(req\/s|RPS)/i.test(bodyText),
hasLatencyData: /\d+\s*(ms|毫秒)/i.test(bodyText),
// 基本頁面結構
hasTitle: pageTitle.length > 0,
hasNavigation: bodyText.includes('戰情室') || bodyText.includes('Dashboard'),
};
// 截圖保存
const screenshotPath = `/tmp/awoooi-qa-${Date.now()}.png`;
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`📸 截圖已保存: ${screenshotPath}`);
// =========================================================================
// 報告輸出
// =========================================================================
console.log('\n============================================================');
console.log(' 🔍 視覺驗證報告');
console.log('============================================================');
console.log(`📄 頁面標題: ${pageTitle}`);
console.log('');
// Console 錯誤報告
console.log('🔴 Console 錯誤:');
if (errors.length === 0) {
console.log(' ✅ 零錯誤 (Clean)');
} else {
console.log(` ❌ 發現 ${errors.length} 個錯誤:`);
errors.slice(0, 10).forEach((e, i) => {
console.log(` ${i + 1}. ${e.substring(0, 100)}...`);
});
}
// Console 警告報告
if (warnings.length > 0) {
console.log(`\n⚠️ Console 警告: ${warnings.length}`);
}
// DOM 數據檢查報告
console.log('\n📊 DOM 數據檢查:');
console.log(` ${checks.hasIncidentId ? '✅' : '❌'} 事件 ID (INC-*)`);
console.log(` ${checks.hasAlertKeywords ? '✅' : '❌'} 告警關鍵字`);
console.log(` ${checks.hasRpsData ? '✅' : '❌'} RPS 數據`);
console.log(` ${checks.hasLatencyData ? '✅' : '❌'} Latency 數據`);
console.log(` ${checks.hasNavigation ? '✅' : '❌'} 導航元素`);
// 最終判定
const hasRealData = checks.hasIncidentId || checks.hasAlertKeywords || checks.hasRpsData;
const isClean = errors.length === 0;
console.log('\n============================================================');
console.log(' 📋 最終判定');
console.log('============================================================');
console.log(`Console 狀態: ${isClean ? '✅ 零紅字' : '❌ 有錯誤'}`);
console.log(`數據渲染狀態: ${hasRealData ? '✅ 有真實數據' : '⚠️ 數據不足'}`);
console.log('');
await browser.close();
// 退出碼決定
if (!isClean) {
console.log('❌ 驗收失敗: Console 存在錯誤');
process.exit(1);
}
// 數據不足不阻塞,但輸出警告
if (!hasRealData) {
console.log('⚠️ 警告: 畫面數據不足,可能需要注入告警或等待 SSE');
}
console.log('✅ 視覺驗證通過');
process.exit(0);
} catch (err) {
console.error('❌ 驗證過程發生錯誤:', err.message);
await browser.close();
process.exit(1);
}
}
runQA().catch((err) => {
console.error('❌ 腳本執行失敗:', err);
process.exit(1);
});