webapp-uat
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb App UAT Skill
Web应用UAT技能
Real browser testing for web applications using Playwright. This skill captures EVERYTHING — console errors, network failures, rendering bugs, broken i18n keys, missing data — and fixes bugs as they're found.
Works with any web stack: React, Vue, Angular, Svelte, Next.js, Nuxt, Ionic/Capacitor, and plain HTML.
使用Playwright对Web应用进行真实浏览器测试。该技能会捕获所有问题——控制台错误、网络失败、渲染Bug、无效国际化键、缺失数据——并在发现Bug时立即修复。
适用于任意Web技术栈:React、Vue、Angular、Svelte、Next.js、Nuxt、Ionic/Capacitor以及纯HTML。
CRITICAL RULES
核心规则
- Console errors are bugs. Every , unhandled rejection, and runtime exception MUST be captured and reported. If a console error blocks functionality, FIX IT before continuing.
console.error - Network failures are bugs. 401s, 500s, CORS errors, timeout responses — capture them ALL. Check if the backend is returning proper data or error payloads.
- Visual rendering = truth. Screenshots show what the user actually sees. If a component renders "---", "undefined", "NaN", "[object Object]", or a raw i18n key, that's a bug.
- Backend logs matter. Check server logs for errors that cause frontend skeleton loaders or empty states.
- Fix bugs inline. Don't just report — fix the code, verify the fix compiles, then re-test.
- 控制台错误即Bug。每一条、未处理的Promise拒绝和运行时异常都必须被捕获并上报。如果控制台错误阻碍了功能运行,需先修复再继续测试。
console.error - 网络失败即Bug。401、500、CORS错误、超时响应——所有此类问题都要捕获。检查后端是否返回了正确的数据或错误负载。
- 视觉渲染为事实依据。截图展示用户实际看到的内容。如果组件渲染出"---"、"undefined"、"NaN"、"[object Object]"或原始国际化键,这就是Bug。
- 后端日志至关重要。检查服务器日志,找出导致前端骨架加载器或空状态的错误。
- 在线修复Bug。不要只上报——修改代码,验证修复后的代码可编译,然后重新测试。
Prerequisites
前置条件
- Playwright installed: (v1.40+)
npx playwright --version - Chromium browser: if needed
npx playwright install chromium - Frontend running (default — override with
http://localhost:3000)BASE_URL - Backend running (default — override with
http://localhost:4000)BACKEND_URL
- 已安装Playwright:(v1.40+)
npx playwright --version - 已安装Chromium浏览器:若需要,执行
npx playwright install chromium - 前端已启动(默认地址——可通过
http://localhost:3000覆盖)BASE_URL - 后端已启动(默认地址——可通过
http://localhost:4000覆盖)BACKEND_URL
Getting Started
快速开始
Before running UAT, the skill needs to understand your app. It will:
- Auto-detect your stack by reading , framework configs, and route definitions
package.json - Build a screen checklist from your routes/pages
- Identify auth strategy from your code (JWT, cookies, OAuth, etc.)
If your project has a in the root, the skill uses it directly. Otherwise, it auto-discovers screens and asks you to confirm.
uat.config.js在运行UAT之前,该技能需要先了解你的应用。它会:
- 自动检测技术栈:读取、框架配置和路由定义
package.json - 生成页面检查清单:基于你的路由/页面
- 识别认证策略:从代码中判断(JWT、Cookie、OAuth等)
如果项目根目录下有,技能会直接使用该配置。否则,它会自动发现页面并请求你确认。
uat.config.jsUAT Config (Optional)
UAT配置(可选)
Create in your project root for repeatable runs:
uat.config.jsjavascript
module.exports = {
// Base URLs
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
backendUrl: process.env.BACKEND_URL || 'http://localhost:4000',
// Browser settings
viewport: { width: 1440, height: 900 },
colorScheme: 'dark', // 'dark' | 'light' | 'no-preference'
headless: true,
// Authentication (pick one)
auth: {
// Option A: Reuse saved browser state (cookies, localStorage)
storageState: '/tmp/uat-auth-state.json',
// Option B: Login programmatically
// login: async (page) => {
// await page.goto('/login');
// await page.fill('input[type="email"]', process.env.TEST_EMAIL);
// await page.fill('input[type="password"]', process.env.TEST_PASSWORD);
// await page.click('button[type="submit"]');
// await page.waitForURL('**/dashboard', { timeout: 15000 });
// },
// Option C: Open headed browser for manual login
// interactive: true,
},
// Health check endpoints (verified before UAT starts)
healthChecks: [
'/health',
// '/api/ping',
],
// Screens to test — each screen gets a full pass
screens: [
{
name: 'Home',
path: '/',
checks: [
'page loads without console errors',
'page title is set',
'main content renders (not empty/skeleton)',
],
},
{
name: 'Dashboard',
path: '/dashboard',
checks: [
'data renders with real values (not placeholders)',
'charts/graphs render (canvas/svg has dimensions > 0)',
'no failed API calls',
],
},
// Add your screens...
],
// Mobile viewport for responsive testing
mobileViewport: { width: 390, height: 844 },
// Screenshots directory
screenshotDir: '/tmp/uat-screenshots',
// i18n settings (set to null to skip i18n checks)
i18n: {
framework: 'auto', // 'i18next' | 'react-intl' | 'vue-i18n' | 'auto' | null
},
};在项目根目录创建以实现可重复运行:
uat.config.jsjavascript
module.exports = {
// 基础URL
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
backendUrl: process.env.BACKEND_URL || 'http://localhost:4000',
// 浏览器设置
viewport: { width: 1440, height: 900 },
colorScheme: 'dark', // 'dark' | 'light' | 'no-preference'
headless: true,
// 身份验证(选其一)
auth: {
// 选项A:复用已保存的浏览器状态(Cookie、localStorage)
storageState: '/tmp/uat-auth-state.json',
// 选项B:程序化登录
// login: async (page) => {
// await page.goto('/login');
// await page.fill('input[type="email"]', process.env.TEST_EMAIL);
// await page.fill('input[type="password"]', process.env.TEST_PASSWORD);
// await page.click('button[type="submit"]');
// await page.waitForURL('**/dashboard', { timeout: 15000 });
// },
// 选项C:打开可视化浏览器进行手动登录
// interactive: true,
},
// 健康检查端点(UAT开始前验证)
healthChecks: [
'/health',
// '/api/ping',
],
// 待测试页面——每个页面都会完成完整检查
screens: [
{
name: '首页',
path: '/',
checks: [
'页面加载无控制台错误',
'页面标题已设置',
'主内容已渲染(非空/骨架状态)',
],
},
{
name: '仪表盘',
path: '/dashboard',
checks: [
'数据渲染为真实值(非占位符)',
'图表已渲染(canvas/svg尺寸>0)',
'无失败的API调用',
],
},
// 添加更多页面...
],
// 响应式测试的移动端视口
mobileViewport: { width: 390, height: 844 },
// 截图目录
screenshotDir: '/tmp/uat-screenshots',
// 国际化设置(设为null可跳过国际化检查)
i18n: {
framework: 'auto', // 'i18next' | 'react-intl' | 'vue-i18n' | 'auto' | null
},
};Authentication
身份验证
Option A: Reuse Saved Session (recommended)
选项A:复用已保存会话(推荐)
Run the login helper once in headed mode, then reuse the state:
javascript
// Save auth state after manual login
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(BASE_URL);
// ... manual login happens ...
await context.storageState({ path: '/tmp/uat-auth-state.json' });在可视化模式下运行一次登录助手,然后复用状态:
javascript
// 手动登录后保存认证状态
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(BASE_URL);
// ... 执行手动登录 ...
await context.storageState({ path: '/tmp/uat-auth-state.json' });Option B: Programmatic Login
选项B:程序化登录
javascript
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
await page.goto(`${BASE_URL}/login`);
await page.fill('input[type="email"]', process.env.TEST_EMAIL);
await page.fill('input[type="password"]', process.env.TEST_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard', { timeout: 15000 });javascript
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
await page.goto(`${BASE_URL}/login`);
await page.fill('input[type="email"]', process.env.TEST_EMAIL);
await page.fill('input[type="password"]', process.env.TEST_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard', { timeout: 15000 });Option C: Interactive Login
选项C:交互式登录
bash
undefinedbash
undefinedOpens a browser window for manual login, saves state
打开浏览器窗口进行手动登录并保存状态
node assets/login-helper.js
undefinednode assets/login-helper.js
undefinedUAT Script Pattern
UAT脚本模板
Every UAT run follows this structure:
javascript
const { chromium } = require('playwright');
const {
setupErrorCapture, screenshot, waitForSettle,
checkBrokenI18n, checkA11y, checkEmptyData, printReport
} = require('./assets/test-helper');
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
async function run() {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
storageState: '/tmp/uat-auth-state.json',
viewport: { width: 1440, height: 900 },
});
const page = await context.newPage();
const errors = setupErrorCapture(page);
// ═══ SCREEN 1: Navigate, settle, check, screenshot ═══
await page.goto(`${BASE_URL}/`);
await waitForSettle(page);
const a11y = await checkA11y(page);
const i18n = await checkBrokenI18n(page);
const empty = await checkEmptyData(page);
await screenshot(page, '01-home');
printReport('Home', {
'Page loads': true,
'No console errors': errors.console.length === 0,
'Single h1': a11y.h1Count === 1,
'Has <main>': a11y.hasMain,
'No broken i18n': i18n.length === 0,
'No empty data': empty.length === 0,
}, errors);
// ═══ REPEAT FOR EACH SCREEN ═══
// ═══ FINAL REPORT ═══
console.log('\n═══ UAT SUMMARY ═══');
console.log(`Console errors: ${errors.console.length}`);
errors.console.forEach(e => console.log(` ❌ [${e.url}] ${e.text.substring(0, 200)}`));
console.log(`Network errors: ${errors.network.length}`);
errors.network.forEach(e => console.log(` 🔴 HTTP ${e.status}: ${e.reqUrl}`));
console.log(`Page errors: ${errors.pageErrors.length}`);
console.log(`Warnings: ${errors.warnings.length}`);
await browser.close();
}
run().catch(err => {
console.error('UAT CRASHED:', err.message);
process.exit(1);
});每次UAT运行都遵循以下结构:
javascript
const { chromium } = require('playwright');
const {
setupErrorCapture, screenshot, waitForSettle,
checkBrokenI18n, checkA11y, checkEmptyData, printReport
} = require('./assets/test-helper');
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
async function run() {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
storageState: '/tmp/uat-auth-state.json',
viewport: { width: 1440, height: 900 },
});
const page = await context.newPage();
const errors = setupErrorCapture(page);
// ═══ 页面1:导航、等待稳定、检查、截图 ═══
await page.goto(`${BASE_URL}/`);
await waitForSettle(page);
const a11y = await checkA11y(page);
const i18n = await checkBrokenI18n(page);
const empty = await checkEmptyData(page);
await screenshot(page, '01-home');
printReport('首页', {
'页面加载完成': true,
'无控制台错误': errors.console.length === 0,
'单个h1标签': a11y.h1Count === 1,
'包含<main>标签': a11y.hasMain,
'无无效国际化键': i18n.length === 0,
'无空数据': empty.length === 0,
}, errors);
// ═══ 为每个页面重复上述步骤 ═══
// ═══ 最终报告 ═══
console.log('\n═══ UAT总结 ═══');
console.log(`控制台错误数:${errors.console.length}`);
errors.console.forEach(e => console.log(` ❌ [${e.url}] ${e.text.substring(0, 200)}`));
console.log(`网络错误数:${errors.network.length}`);
errors.network.forEach(e => console.log(` 🔴 HTTP ${e.status}: ${e.reqUrl}`));
console.log(`页面错误数:${errors.pageErrors.length}`);
console.log(`警告数:${errors.warnings.length}`);
await browser.close();
}
run().catch(err => {
console.error('UAT崩溃:', err.message);
process.exit(1);
});Screen Testing Methodology
页面测试方法论
For each screen in the checklist:
- Navigate —
await page.goto(url) - Settle — (network idle + render delay)
await waitForSettle(page) - Capture — screenshot the initial state
- Validate — run all checks:
- — landmarks, headings, focus targets
checkA11y(page) - — raw keys, unresolved placeholders
checkBrokenI18n(page) - — placeholder values in data cells
checkEmptyData(page) - Custom checks per screen (data loaded, charts rendered, etc.)
- Interact — test key user flows (click, type, navigate)
- Report — with pass/fail per check
printReport()
对于检查清单中的每个页面:
- 导航 —
await page.goto(url) - 等待稳定 — (网络空闲 + 渲染延迟)
await waitForSettle(page) - 捕获 — 截图初始状态
- 验证 — 执行所有检查:
- — 地标、标题、焦点目标
checkA11y(page) - — 原始键、未解析占位符
checkBrokenI18n(page) - — 数据单元格中的占位符值
checkEmptyData(page) - 页面专属自定义检查(数据已加载、图表已渲染等)
- 交互 — 测试关键用户流程(点击、输入、导航)
- 报告 — 使用输出每项检查的通过/失败状态
printReport()
Universal Checks (Every Screen)
通用检查项(所有页面)
Accessibility (WCAG 2.2 AA)
无障碍(WCAG 2.2 AA标准)
- Tab through entire page — focus ring visible on every interactive element
- Exactly one per page
<h1> - or
<main>landmark present[role="main"] - has
<nav>aria-label - All elements have
<img>attributesalt - No — interactive elements must be
<div onclick>or<button><a> - Touch targets >= 44x44px (mobile)
- Color contrast meets 4.5:1 ratio
- 按Tab键遍历整个页面——每个交互元素都有可见的焦点环
- 每个页面恰好有一个标签
<h1> - 存在或
<main>地标[role="main"] - 标签带有
<nav>属性aria-label - 所有元素都有
<img>属性alt - 无——交互元素必须是
<div onclick>或<button><a> - 触摸目标尺寸≥44x44px(移动端)
- 颜色对比度符合4.5:1比例
i18n / Localization
国际化(i18n)/本地化
- No raw keys visible (e.g., ,
KEY 'FOO.BAR',t('key'))$t('key') - No unresolved or
{{variable}}placeholders{variable} - Date/number formatting matches locale
- Locale switch updates all visible text (if applicable)
- 无可见原始键(如、
KEY 'FOO.BAR'、t('key'))$t('key') - 无未解析的或
{{variable}}占位符{variable} - 日期/数字格式符合区域设置
- 切换区域设置时更新所有可见文本(若适用)
Data Integrity
数据完整性
- No placeholder values: "---", "NaN", "undefined", "null", "[object Object]", "$0.00"
- Loading states resolve to real content (no infinite skeletons)
- Empty states are intentional (show a message, not blank space)
- 无占位符值:"---"、"NaN"、"undefined"、"null"、"[object Object]"、"$0.00"
- 加载状态最终会渲染为真实内容(无无限骨架加载)
- 空状态是有意设计的(显示提示信息,而非空白区域)
Responsive (Mobile Viewport)
响应式(移动端视口)
- No horizontal scrollbar at 390px width
- Navigation is accessible (hamburger menu, tab bar, etc.)
- Text is readable without zooming
- Modals/dialogs fit within viewport
- 宽度为390px时无水平滚动条
- 导航可访问(汉堡菜单、标签栏等)
- 文本无需缩放即可阅读
- 模态框/对话框适配视口
Performance
性能
- Page settles within 5 seconds
- No infinite API polling (check network tab)
- No memory leaks from repeated navigation (console warnings)
- 页面在5秒内稳定
- 无无限API轮询(检查网络面板)
- 重复导航无内存泄漏(控制台警告)
Bug Triage
Bug分类
When a bug is found:
- Screenshot it —
await screenshot(page, 'BUG-description') - Capture console — log the exact error text and stack trace
- Identify root cause — read the source file, trace the data flow
- Classify severity:
- P0 BLOCKER: App won't load, screen completely broken, data loss risk
- P1 HIGH: Feature doesn't work, wrong data displayed, accessibility barrier
- P2 MEDIUM: Visual glitch, missing data that has a fallback, minor a11y issue
- P3 LOW: Cosmetic, console warning, edge case
- Fix P0/P1 immediately — edit the code, verify compilation, re-test
- Log P2/P3 — report in summary, fix after completing full screen pass
发现Bug时:
- 截图 —
await screenshot(page, 'BUG-description') - 捕获控制台信息 — 记录准确的错误文本和堆栈跟踪
- 确定根本原因 — 阅读源文件,追踪数据流
- 分类严重程度:
- P0 阻塞级:应用无法加载、页面完全损坏、存在数据丢失风险
- P1 高优先级:功能无法使用、显示错误数据、存在无障碍障碍
- P2 中优先级:视觉故障、有回退方案的缺失数据、轻微无障碍问题
- P3 低优先级: cosmetic问题、控制台警告、边缘情况
- 立即修复P0/P1级Bug — 修改代码,验证编译,重新测试
- 记录P2/P3级Bug — 在总结中上报,完成所有页面测试后再修复
Backend Health Pre-Check
后端健康预检
Before testing screens, verify the backend is alive:
javascript
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:4000';
async function checkBackendHealth(endpoints = ['/health']) {
console.log('═══ Backend Health ═══');
for (const ep of endpoints) {
try {
const res = await fetch(`${BACKEND_URL}${ep}`);
const status = res.status < 400 ? '✅' : '❌';
console.log(` ${status} ${ep}: HTTP ${res.status}`);
} catch (e) {
console.log(` ❌ ${ep}: UNREACHABLE — ${e.message}`);
}
}
}测试页面之前,验证后端是否正常运行:
javascript
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:4000';
async function checkBackendHealth(endpoints = ['/health']) {
console.log('═══ 后端健康检查 ═══');
for (const ep of endpoints) {
try {
const res = await fetch(`${BACKEND_URL}${ep}`);
const status = res.status < 400 ? '✅' : '❌';
console.log(` ${status} ${ep}: HTTP ${res.status}`);
} catch (e) {
console.log(` ❌ ${ep}: 无法访问 — ${e.message}`);
}
}
}Post-UAT Report
UAT后报告
After completing all screens, generate a report with:
-
Per-screen scores (1-10) based on:
- Functionality: Does it work? (40%)
- Data accuracy: Are real values shown? (25%)
- Accessibility: Keyboard, screen reader, contrast (20%)
- Visual quality: Layout, spacing, responsive (15%)
-
All bugs found — severity, file, line, fix status
-
Overall health score — weighted average across all screens
-
Recommendations — prioritized list of fixes for next sprint
完成所有页面测试后,生成包含以下内容的报告:
-
页面得分(1-10分),基于:
- 功能性:是否可用?(40%)
- 数据准确性:是否显示真实值?(25%)
- 无障碍性:键盘、屏幕阅读器、对比度(20%)
- 视觉质量:布局、间距、响应式(15%)
-
所有发现的Bug — 严重程度、文件、行号、修复状态
-
整体健康得分 — 所有页面的加权平均分
-
建议 — 下一个迭代周期的优先修复列表
Framework-Specific Tips
各框架专属提示
React (CRA, Vite, Next.js)
React(CRA、Vite、Next.js)
- Wait for hydration: after navigation
waitForSettle(page, 2000) - Check for React error boundaries rendering fallback UI
- DevTools warnings about keys, deprecated lifecycle methods are worth logging
- 等待 hydration 完成:导航后执行
waitForSettle(page, 2000) - 检查React错误边界是否渲染了回退UI
- DevTools中关于键、已弃用生命周期方法的警告值得记录
Vue (Nuxt, Vite)
Vue(Nuxt、Vite)
- can cause flash of missing content — screenshot after settle
v-if - Check calls resolve (vue-i18n)
$t()
- 可能导致内容闪烁——等待稳定后再截图
v-if - 检查调用是否解析成功(vue-i18n)
$t()
Angular
Angular
- Zone.js may keep network "busy" — use with longer timeout
waitForSettle - Check for attributes leaking into production builds
ng-reflect-*
- Zone.js可能导致网络持续处于"繁忙"状态——使用并设置更长超时时间
waitForSettle - 检查生产构建中是否泄漏了属性
ng-reflect-*
Ionic / Capacitor (Hybrid Mobile)
Ionic / Capacitor(混合移动端)
- Test with mobile viewport (390x844) as primary
- scrolling may differ from native scroll
ion-content - Safe area insets: check content isn't hidden behind notch/home indicator
- Test ,
ion-modaldismiss behaviorsion-action-sheet - Hardware back button simulation:
page.goBack()
- 优先使用移动端视口(390x844)测试
- 的滚动行为可能与原生滚动不同
ion-content - 安全区域内边距:检查内容是否被凹槽/主页指示器遮挡
- 测试、
ion-modal的关闭行为ion-action-sheet - 模拟硬件返回按钮:
page.goBack()
Next.js / Nuxt (SSR)
Next.js / Nuxt(SSR)
- First paint may differ from hydrated state — screenshot both
- Check for hydration mismatch warnings in console
- API routes: test endpoints in health check
/api/*
- 首次渲染可能与hydrated状态不同——两种状态都要截图
- 检查控制台中的hydration不匹配警告
- API路由:在健康检查中测试端点
/api/*