Web Tester
Browser-based frontend testing with Playwright. Test user flows, validate UI behavior, and detect visual regressions.
Features
- Browser automation - Navigate, click, fill forms, take screenshots
- Assertions - Collect all test results, report failures at end
- Visual diff - Compare screenshots against baselines with perceptual diff
- Dev server detection - Auto-detect running local servers
Setup (First Time)
cd $SKILL_DIR && npm run setup
This installs Playwright and Chromium. Only needed once.
Quick Start
1. Detect dev servers
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(s => console.log(JSON.stringify(s)))"
2. Write test to /tmp
// /tmp/test-homepage.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await helpers.createContext(browser);
const page = await context.newPage();
// Create test with assertions
const test = helpers.createTest('Homepage Test');
await page.goto('http://localhost:3000');
test.assert(await page.title() !== '', 'Page has title');
await test.assertVisible(page, 'nav', 'Navigation is visible');
await test.assertText(page, 'h1', 'Welcome', 'Hero heading contains Welcome');
// Visual regression check
await helpers.compareScreenshot(page, 'homepage');
await test.finish();
await browser.close();
})();
3. Execute
cd $SKILL_DIR && node run.js /tmp/test-homepage.js
Assertions API
Create a test to collect assertions:
const test = helpers.createTest('My Test');
// Basic assertion
test.assert(condition, 'Description');
// Element visibility
await test.assertVisible(page, 'selector', 'Description');
// Text content
await test.assertText(page, 'selector', 'expected text', 'Description');
// URL check
await test.assertUrl(page, '/dashboard', 'Description');
// Element count
await test.assertCount(page, '.item', 5, 'Description');
// Attribute check
await test.assertAttribute(page, 'input', 'type', 'email', 'Description');
// Finish and save results
await test.finish();
Results are saved to /tmp/test-results.json.
View results
cd $SKILL_DIR && node run.js --show-results
Clear results
cd $SKILL_DIR && node run.js --clear-results
Visual Regression API
Compare screenshots against baselines:
await helpers.compareScreenshot(page, 'screenshot-name', {
threshold: 0.5, // Diff threshold % (default: 0.5)
baselineDir: './baselines', // Baseline folder (default: ./visual-baselines)
fullPage: true // Full page screenshot (default: true)
});
Baseline workflow
- First run - Creates baseline, marks as "pending approval"
- Subsequent runs - Compares against baseline
- If different - Saves
.current.pngand.diff.pngfor review
Manage baselines
# List all baselines and their status
cd $SKILL_DIR && node run.js --list-baselines
# Approve all pending baselines (with confirmation)
cd $SKILL_DIR && node run.js --approve-baselines
# Approve specific baseline
cd $SKILL_DIR && node run.js --approve-baseline homepage
# Reject baseline (keep old, delete current/diff)
cd $SKILL_DIR && node run.js --reject-baseline homepage
Common Patterns
Test login flow
// /tmp/test-login.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const test = helpers.createTest('Login Flow');
await page.goto('http://localhost:3000/login');
await helpers.safeType(page, 'input[name="email"]', 'test@example.com');
await helpers.safeType(page, 'input[name="password"]', 'password123');
await helpers.safeClick(page, 'button[type="submit"]');
await page.waitForURL('**/dashboard');
await test.assertUrl(page, '/dashboard', 'Redirected to dashboard');
await test.assertVisible(page, '.user-menu', 'User menu visible');
await test.finish();
await browser.close();
})();
Test responsive design
// /tmp/test-responsive.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
(async () => {
const browser = await chromium.launch({ headless: false });
const test = helpers.createTest('Responsive Design');
const viewports = [
{ name: 'desktop', width: 1920, height: 1080 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'mobile', width: 375, height: 667 }
];
for (const vp of viewports) {
const context = await browser.newContext({ viewport: vp });
const page = await context.newPage();
await page.goto('http://localhost:3000');
await helpers.compareScreenshot(page, `homepage-${vp.name}`);
await test.assertVisible(page, 'nav', `Nav visible on ${vp.name}`);
await context.close();
}
await test.finish();
await browser.close();
})();
Test form validation
// /tmp/test-form.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const test = helpers.createTest('Form Validation');
await page.goto('http://localhost:3000/contact');
// Submit empty form
await helpers.safeClick(page, 'button[type="submit"]');
await test.assertVisible(page, '.error-message', 'Shows error for empty form');
// Fill and submit
await helpers.safeType(page, 'input[name="email"]', 'test@example.com');
await helpers.safeType(page, 'textarea[name="message"]', 'Hello');
await helpers.safeClick(page, 'button[type="submit"]');
await test.assertVisible(page, '.success-message', 'Shows success message');
await test.finish();
await browser.close();
})();
Helper Functions
const helpers = require('./lib/helpers');
// Browser setup
await helpers.launchBrowser('chromium', { headless: false });
await helpers.createContext(browser, { viewport: { width: 1920, height: 1080 } });
await helpers.createPage(context);
// Dev server detection
const servers = await helpers.detectDevServers();
// Safe interactions (with retry)
await helpers.safeClick(page, 'button', { retries: 3 });
await helpers.safeType(page, 'input', 'text', { clear: true });
// Page utilities
await helpers.waitForPageReady(page, { waitUntil: 'networkidle' });
await helpers.takeScreenshot(page, 'my-screenshot');
await helpers.handleCookieBanner(page);
await helpers.scrollPage(page, 'bottom');
// Data extraction
const texts = await helpers.extractTexts(page, '.list-item');
const tableData = await helpers.extractTableData(page, 'table');
Inline Execution
For quick tests, run inline code:
cd $SKILL_DIR && node run.js "
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
console.log('Title:', await page.title());
await takeScreenshot(page, 'quick-check');
await browser.close();
"
Environment Variables
| Variable | Description |
|----------|-------------|
| VISUAL_BASELINE_DIR | Baseline directory (default: ./visual-baselines) |
| HEADLESS | Run browser headless if true (default: false) |
| SLOW_MO | Slow down actions by ms |
| PW_HEADER_NAME + PW_HEADER_VALUE | Add custom HTTP header |
| PW_EXTRA_HEADERS | JSON object of extra headers |
CLI Reference
# Execute test file
node run.js /tmp/test.js
# Execute inline code
node run.js "await page.goto('...')"
# Baseline management
node run.js --list-baselines
node run.js --approve-baselines
node run.js --approve-baseline <name>
node run.js --reject-baseline <name>
# Results management
node run.js --show-results
node run.js --clear-results
# Help
node run.js --help
Tips
- Always detect servers first for localhost testing
- Write tests to /tmp to avoid cluttering projects
- Use
headless: falseby default for easier debugging - Visual baselines need human approval before becoming authoritative
- Assertions continue on failure - all results reported at end