webapp-uat

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web 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

核心规则

  1. Console errors are bugs. Every
    console.error
    , unhandled rejection, and runtime exception MUST be captured and reported. If a console error blocks functionality, FIX IT before continuing.
  2. Network failures are bugs. 401s, 500s, CORS errors, timeout responses — capture them ALL. Check if the backend is returning proper data or error payloads.
  3. 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.
  4. Backend logs matter. Check server logs for errors that cause frontend skeleton loaders or empty states.
  5. Fix bugs inline. Don't just report — fix the code, verify the fix compiles, then re-test.
  1. 控制台错误即Bug。每一条
    console.error
    、未处理的Promise拒绝和运行时异常都必须被捕获并上报。如果控制台错误阻碍了功能运行,需先修复再继续测试。
  2. 网络失败即Bug。401、500、CORS错误、超时响应——所有此类问题都要捕获。检查后端是否返回了正确的数据或错误负载。
  3. 视觉渲染为事实依据。截图展示用户实际看到的内容。如果组件渲染出"---"、"undefined"、"NaN"、"[object Object]"或原始国际化键,这就是Bug。
  4. 后端日志至关重要。检查服务器日志,找出导致前端骨架加载器或空状态的错误。
  5. 在线修复Bug。不要只上报——修改代码,验证修复后的代码可编译,然后重新测试。

Prerequisites

前置条件

  • Playwright installed:
    npx playwright --version
    (v1.40+)
  • Chromium browser:
    npx playwright install chromium
    if needed
  • Frontend running (default
    http://localhost:3000
    — override with
    BASE_URL
    )
  • Backend running (default
    http://localhost:4000
    — override with
    BACKEND_URL
    )
  • 已安装Playwright:
    npx playwright --version
    (v1.40+)
  • 已安装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:
  1. Auto-detect your stack by reading
    package.json
    , framework configs, and route definitions
  2. Build a screen checklist from your routes/pages
  3. Identify auth strategy from your code (JWT, cookies, OAuth, etc.)
If your project has a
uat.config.js
in the root, the skill uses it directly. Otherwise, it auto-discovers screens and asks you to confirm.
在运行UAT之前,该技能需要先了解你的应用。它会:
  1. 自动检测技术栈:读取
    package.json
    、框架配置和路由定义
  2. 生成页面检查清单:基于你的路由/页面
  3. 识别认证策略:从代码中判断(JWT、Cookie、OAuth等)
如果项目根目录下有
uat.config.js
,技能会直接使用该配置。否则,它会自动发现页面并请求你确认。

UAT Config (Optional)

UAT配置(可选)

Create
uat.config.js
in your project root for repeatable runs:
javascript
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.js
以实现可重复运行:
javascript
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
undefined
bash
undefined

Opens a browser window for manual login, saves state

打开浏览器窗口进行手动登录并保存状态

node assets/login-helper.js
undefined
node assets/login-helper.js
undefined

UAT 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:
  1. Navigate
    await page.goto(url)
  2. Settle
    await waitForSettle(page)
    (network idle + render delay)
  3. Capture — screenshot the initial state
  4. Validate — run all checks:
    • checkA11y(page)
      — landmarks, headings, focus targets
    • checkBrokenI18n(page)
      — raw keys, unresolved placeholders
    • checkEmptyData(page)
      — placeholder values in data cells
    • Custom checks per screen (data loaded, charts rendered, etc.)
  5. Interact — test key user flows (click, type, navigate)
  6. Report
    printReport()
    with pass/fail per check
对于检查清单中的每个页面:
  1. 导航
    await page.goto(url)
  2. 等待稳定
    await waitForSettle(page)
    (网络空闲 + 渲染延迟)
  3. 捕获 — 截图初始状态
  4. 验证 — 执行所有检查:
    • checkA11y(page)
      — 地标、标题、焦点目标
    • checkBrokenI18n(page)
      — 原始键、未解析占位符
    • checkEmptyData(page)
      — 数据单元格中的占位符值
    • 页面专属自定义检查(数据已加载、图表已渲染等)
  5. 交互 — 测试关键用户流程(点击、输入、导航)
  6. 报告 — 使用
    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
    <h1>
    per page
  • <main>
    or
    [role="main"]
    landmark present
  • <nav>
    has
    aria-label
  • All
    <img>
    elements have
    alt
    attributes
  • No
    <div onclick>
    — interactive elements must be
    <button>
    or
    <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
    {{variable}}
    or
    {variable}
    placeholders
  • 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:
  1. Screenshot it
    await screenshot(page, 'BUG-description')
  2. Capture console — log the exact error text and stack trace
  3. Identify root cause — read the source file, trace the data flow
  4. 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
  5. Fix P0/P1 immediately — edit the code, verify compilation, re-test
  6. Log P2/P3 — report in summary, fix after completing full screen pass
发现Bug时:
  1. 截图
    await screenshot(page, 'BUG-description')
  2. 捕获控制台信息 — 记录准确的错误文本和堆栈跟踪
  3. 确定根本原因 — 阅读源文件,追踪数据流
  4. 分类严重程度:
    • P0 阻塞级:应用无法加载、页面完全损坏、存在数据丢失风险
    • P1 高优先级:功能无法使用、显示错误数据、存在无障碍障碍
    • P2 中优先级:视觉故障、有回退方案的缺失数据、轻微无障碍问题
    • P3 低优先级: cosmetic问题、控制台警告、边缘情况
  5. 立即修复P0/P1级Bug — 修改代码,验证编译,重新测试
  6. 记录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:
  1. 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%)
  2. All bugs found — severity, file, line, fix status
  3. Overall health score — weighted average across all screens
  4. Recommendations — prioritized list of fixes for next sprint
完成所有页面测试后,生成包含以下内容的报告:
  1. 页面得分(1-10分),基于:
    • 功能性:是否可用?(40%)
    • 数据准确性:是否显示真实值?(25%)
    • 无障碍性:键盘、屏幕阅读器、对比度(20%)
    • 视觉质量:布局、间距、响应式(15%)
  2. 所有发现的Bug — 严重程度、文件、行号、修复状态
  3. 整体健康得分 — 所有页面的加权平均分
  4. 建议 — 下一个迭代周期的优先修复列表

Framework-Specific Tips

各框架专属提示

React (CRA, Vite, Next.js)

React(CRA、Vite、Next.js)

  • Wait for hydration:
    waitForSettle(page, 2000)
    after navigation
  • 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)

  • v-if
    can cause flash of missing content — screenshot after settle
  • Check
    $t()
    calls resolve (vue-i18n)
  • v-if
    可能导致内容闪烁——等待稳定后再截图
  • 检查
    $t()
    调用是否解析成功(vue-i18n)

Angular

Angular

  • Zone.js may keep network "busy" — use
    waitForSettle
    with longer timeout
  • Check for
    ng-reflect-*
    attributes leaking into production builds
  • Zone.js可能导致网络持续处于"繁忙"状态——使用
    waitForSettle
    并设置更长超时时间
  • 检查生产构建中是否泄漏了
    ng-reflect-*
    属性

Ionic / Capacitor (Hybrid Mobile)

Ionic / Capacitor(混合移动端)

  • Test with mobile viewport (390x844) as primary
  • ion-content
    scrolling may differ from native scroll
  • Safe area insets: check content isn't hidden behind notch/home indicator
  • Test
    ion-modal
    ,
    ion-action-sheet
    dismiss behaviors
  • 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
    /api/*
    endpoints in health check
  • 首次渲染可能与hydrated状态不同——两种状态都要截图
  • 检查控制台中的hydration不匹配警告
  • API路由:在健康检查中测试
    /api/*
    端点