# Webapp Uat

> Full browser UAT for web apps — Playwright testing with console/network error capture, accessibility checks, i18n validation, and bug triage. Use when running screen-by-screen UAT or testing specific features in any web or hybrid app (React, Vue, Angular, Ionic, Next.js, etc).

- Skill: `tsilverberg/webapp-uat-2` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add tsilverberg/webapp-uat-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tsilverberg/webapp-uat-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tsilverberg (https://skillmd.com/u/tsilverberg)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tsilverberg/webapp-uat-2

---


# Web App UAT Skill

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.

## 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.

## 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`)

## 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 Config (Optional)

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
  },
};
```

## Authentication

### Option A: Reuse Saved Session (recommended)

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' });
```

### Option B: Programmatic Login

```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

```bash
# Opens a browser window for manual login, saves state
node assets/login-helper.js
```

## UAT Script Pattern

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);
});
```

## 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

## Universal Checks (Every Screen)

### Accessibility (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

### i18n / Localization
- [ ] 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)

### 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)

### 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

### Performance
- [ ] Page settles within 5 seconds
- [ ] No infinite API polling (check network tab)
- [ ] No memory leaks from repeated navigation (console warnings)

## Bug Triage

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

## 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}`);
    }
  }
}
```

## Post-UAT Report

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

## Framework-Specific Tips

### 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

### Vue (Nuxt, Vite)
- `v-if` can cause flash of missing content — screenshot after settle
- Check `$t()` calls resolve (vue-i18n)

### Angular
- Zone.js may keep network "busy" — use `waitForSettle` with longer timeout
- Check for `ng-reflect-*` attributes leaking into production builds

### Ionic / Capacitor (Hybrid Mobile)
- 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()`

### 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

