Webapp-uat 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).
git clone https://github.com/tsilverberg/webapp-uat
T=$(mktemp -d) && git clone --depth=1 https://github.com/tsilverberg/webapp-uat "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.agents/skills/webapp-uat" ~/.claude/skills/tsilverberg-webapp-uat-webapp-uat && rm -rf "$T"
.agents/skills/webapp-uat/SKILL.mdWeb 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
- 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.
Prerequisites
- Playwright installed:
(v1.40+)npx playwright --version - Chromium browser:
if needednpx playwright install chromium - Frontend running (default
— override withhttp://localhost:3000
)BASE_URL - Backend running (default
— override withhttp://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 definitionspackage.json - Build a screen checklist from your routes/pages
- 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:
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:
// 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
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
# Opens a browser window for manual login, saves state node assets/login-helper.js
UAT Script Pattern
Every UAT run follows this structure:
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:
- 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 targetscheckA11y(page)
— raw keys, unresolved placeholderscheckBrokenI18n(page)
— placeholder values in data cellscheckEmptyData(page)- Custom checks per screen (data loaded, charts rendered, etc.)
- Interact — test key user flows (click, type, navigate)
- Report —
with pass/fail per checkprintReport()
Universal Checks (Every Screen)
Accessibility (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
i18n / Localization
- 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)
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:
- 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
Backend Health Pre-Check
Before testing screens, verify the backend is alive:
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:
-
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
Framework-Specific Tips
React (CRA, Vite, Next.js)
- Wait for hydration:
after navigationwaitForSettle(page, 2000) - Check for React error boundaries rendering fallback UI
- DevTools warnings about keys, deprecated lifecycle methods are worth logging
Vue (Nuxt, Vite)
can cause flash of missing content — screenshot after settlev-if- Check
calls resolve (vue-i18n)$t()
Angular
- Zone.js may keep network "busy" — use
with longer timeoutwaitForSettle - Check for
attributes leaking into production buildsng-reflect-*
Ionic / Capacitor (Hybrid Mobile)
- Test with mobile viewport (390x844) as primary
scrolling may differ from native scrollion-content- Safe area insets: check content isn't hidden behind notch/home indicator
- Test
,ion-modal
dismiss behaviorsion-action-sheet - 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
endpoints in health check/api/*