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>
This commit is contained in:
@@ -28,6 +28,27 @@
|
||||
|
||||
---
|
||||
|
||||
## 🔴 交付前絕對硬性規定 (憲法條款 14/15)
|
||||
|
||||
**任何宣稱「部署完成」或「功能驗收」之前,必須執行以下腳本:**
|
||||
|
||||
```bash
|
||||
# 無頭偵察兵 - Playwright 前端視覺驗證
|
||||
cd apps/web && node scripts/verify-frontend.js
|
||||
```
|
||||
|
||||
### 驗證通過標準
|
||||
|
||||
| 項目 | 標準 |
|
||||
|------|------|
|
||||
| Console 錯誤 | **必須為 0** |
|
||||
| 事件 ID (INC-*) | 建議有 (已注入告警時) |
|
||||
| RPS 數據 | 建議有 (GlobalPulse 已啟用時) |
|
||||
|
||||
**禁止僅依賴 curl 驗收!必須親眼確認 DOM 渲染結果!**
|
||||
|
||||
---
|
||||
|
||||
## Playwright 自動化規範
|
||||
|
||||
### 測試腳本結構
|
||||
|
||||
167
apps/web/scripts/verify-frontend.js
Normal file
167
apps/web/scripts/verify-frontend.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
@@ -32,6 +32,10 @@
|
||||
|
||||
| 時間 | 事件 | 負責人 |
|
||||
|------|------|--------|
|
||||
| 2026-03-23 01:10 | **🚀 Signal Worker 啟用**: `replicas: 0→1` + Redis Streams Consumer 正式上線 + Incident Engine 處理鏈完整 | CTO + Claude Code |
|
||||
| 2026-03-23 01:05 | **🎯 實彈告警發射成功**: 4 發告警注入 Redis Streams (HarborOOMKilled/HighCPU/DBTimeout/RedisMemory) + message_id 確認 | CTO + Claude Code |
|
||||
| 2026-03-23 00:55 | **📊 GlobalPulse 脈搏恢復**: SignOz v3 表修正 + RPS 5.4/Error 25%/P99 3s 真實數據顯示 | CTO + Claude Code |
|
||||
| 2026-03-23 00:45 | **📱 Telegram 通知 UX 升級**: HTML 結構化格式 + Inline Keyboard (查看紀錄/開啟正式站) + 拔除冗長 URL | CTO + Claude Code |
|
||||
| 2026-03-23 00:25 | **🤖 Claude Skills 兵營建置**: 6 大專屬 Skill 模組 (`01-frontend` ~ `06-monorepo`) + Skill Router 寫入主憲法 + Auto-Pilot 驗收機制 | CTO + Claude Code |
|
||||
| 2026-03-23 00:10 | **🔧 OTEL 神經修復**: Port 4317→24317 修正 (SigNoz Host Port) + NetworkPolicy Egress 開通 + ConfigMap 更新 + Traces 正常匯出 | CTO + Claude Code |
|
||||
| 2026-03-22 23:55 | **🔧 Phase 8 NodePort 修復**: NetworkPolicy `allow-nginx-ingress` 新增 K3s Node IP (120/121) + Pod CIDR (10.42.0.0/16) + 502 Bad Gateway 根治 | CTO + Claude Code |
|
||||
|
||||
Reference in New Issue
Block a user