Loading...
Loading...
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).
npx skill4agent add tsilverberg/webapp-uat webapp-uatconsole.errornpx playwright --versionnpx playwright install chromiumhttp://localhost:3000BASE_URLhttp://localhost:4000BACKEND_URLpackage.jsonuat.config.jsuat.config.jsmodule.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
},
};// 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' });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 });# Opens a browser window for manual login, saves state
node assets/login-helper.jsconst { 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);
});await page.goto(url)await waitForSettle(page)checkA11y(page)checkBrokenI18n(page)checkEmptyData(page)printReport()<h1><main>[role="main"]<nav>aria-label<img>alt<div onclick><button><a>KEY 'FOO.BAR't('key')$t('key'){{variable}}{variable}await screenshot(page, 'BUG-description')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}`);
}
}
}waitForSettle(page, 2000)v-if$t()waitForSettleng-reflect-*ion-contention-modalion-action-sheetpage.goBack()/api/*