/** * 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); });