Install
$ agentstack add skill-code-saurabh-openskills-webapp-testing ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Web App Testing (Playwright)
Manual testing finds bugs once. Automated tests find them every time. This skill writes Playwright scripts that test real browser behaviour — clicks, forms, navigation, network calls, and visual state — reliably and repeatably.
Web App Testing Principles
- Reconnaissance before action. Never assume a selector exists. Navigate, wait for load, inspect the DOM, then act. Selectors that work in dev break in staging. Discover them at runtime.
- Wait for real readiness.
networkidleis not always enough. Wait for the specific element you need, not just page load. - Test what the user experiences. Test by role, text, and label — not by CSS class or XPath. Classes change; user-facing text is more stable.
- One test, one user journey. A test that does login + checkout + confirmation + email verification is four tests jammed together. Split them.
- Tests must clean up after themselves. A test that leaves state (created users, placed orders) that affects the next test is a flaky test waiting to happen.
- Screenshot on failure. Every test should capture a screenshot when it fails — this is the single fastest debugging tool available.
Step 0: Choose the Right Approach
Is the app running?
├── NO → Start it first with with_server.py helper (if available)
│ or: npm run dev / python app.py in background
└── YES
└── Is it static HTML (no JS framework)?
├── YES → Read HTML directly, write selectors from source
└── NO (React, Vue, Svelte, etc.)
→ Reconnaissance first: navigate → networkidle → screenshot → inspect → act
Project Setup
Install Playwright
npm init -y
npm install -D @playwright/test
npx playwright install chromium # or: install firefox, webkit too
# Python
pip install playwright
playwright install chromium
Config (playwright.config.ts)
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
timeout: 30_000,
retries: process.env.CI ? 2 : 0, // retry in CI, not locally
use: {
baseURL: process.env.BASE_URL ?? "http://localhost:3000",
headless: true,
screenshot: "only-on-failure", // always capture on failure
video: "retain-on-failure",
trace: "retain-on-failure",
},
webServer: {
command: "npm run dev",
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
Reconnaissance Pattern (Always Do This First)
Before writing any selectors, discover the actual rendered DOM:
import { test, expect } from "@playwright/test";
test("reconnaissance — inspect login page", async ({ page }) => {
await page.goto("/login");
await page.waitForLoadState("networkidle");
// Take screenshot to see what's there
await page.screenshot({ path: "screenshots/login.png", fullPage: true });
// List all interactive elements
const buttons = await page.locator("button").all();
for (const btn of buttons) {
console.log("Button:", await btn.textContent(), await btn.getAttribute("type"));
}
const inputs = await page.locator("input").all();
for (const input of inputs) {
console.log("Input:", await input.getAttribute("name"), await input.getAttribute("type"));
}
});
Run this first. Read the output. Then write your real tests based on what actually exists.
Writing Tests
Preferred Selector Order (most stable → least stable)
// 1. Role + name (most resilient — matches what users see)
page.getByRole("button", { name: "Sign in" })
page.getByRole("textbox", { name: "Email address" })
page.getByRole("link", { name: "Forgot password?" })
// 2. Label (for form fields)
page.getByLabel("Email address")
page.getByLabel("Password")
// 3. Test ID (when team adds them explicitly)
page.getByTestId("submit-button")
// 4. Text (for non-interactive elements)
page.getByText("Welcome back")
// 5. CSS selector (last resort — fragile, breaks on refactor)
page.locator(".login-form button[type='submit']")
// ❌ Never use XPath — brittle, unreadable
page.locator("//button[@class='btn-primary']")
Complete Login Test
import { test, expect } from "@playwright/test";
test.describe("Authentication", () => {
test("user can log in with valid credentials", async ({ page }) => {
await page.goto("/login");
await page.waitForLoadState("networkidle");
// Fill form
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("password123");
// Submit
await page.getByRole("button", { name: "Sign in" }).click();
// Verify redirect to dashboard
await page.waitForURL("**/dashboard");
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});
test("shows error on invalid credentials", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("wrong@example.com");
await page.getByLabel("Password").fill("wrongpassword");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByText("Invalid email or password")).toBeVisible();
// Should NOT redirect
await expect(page).toHaveURL(/.*login.*/);
});
});
Form Submission Test
test("user can submit contact form", async ({ page }) => {
await page.goto("/contact");
await page.waitForLoadState("networkidle");
await page.getByLabel("Name").fill("Jane Doe");
await page.getByLabel("Email").fill("jane@example.com");
await page.getByLabel("Message").fill("This is a test message with sufficient length.");
// Intercept the API call to verify it's made
const responsePromise = page.waitForResponse(
response => response.url().includes("/api/contact") && response.status() === 200
);
await page.getByRole("button", { name: "Send message" }).click();
await responsePromise;
await expect(page.getByText("Message sent successfully")).toBeVisible();
});
Navigation Test
test("main navigation links work", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
const navItems = [
{ link: "Products", url: /.*products.*/ },
{ link: "Pricing", url: /.*pricing.*/ },
{ link: "About", url: /.*about.*/ },
];
for (const { link, url } of navItems) {
await page.getByRole("navigation").getByRole("link", { name: link }).click();
await expect(page).toHaveURL(url);
await page.goBack();
await page.waitForLoadState("networkidle");
}
});
API Interception
test("handles API failure gracefully", async ({ page }) => {
// Mock the API to return an error
await page.route("**/api/users", route => {
route.fulfill({ status: 500, body: JSON.stringify({ error: "Server error" }) });
});
await page.goto("/users");
await page.waitForLoadState("networkidle");
// App should show error state, not crash
await expect(page.getByText("Something went wrong")).toBeVisible();
await expect(page.getByRole("button", { name: "Try again" })).toBeVisible();
});
Visual Testing
Screenshot comparison
test("dashboard looks correct", async ({ page }) => {
await page.goto("/dashboard");
await page.waitForLoadState("networkidle");
// Wait for data to load before snapping
await page.waitForSelector("[data-testid='chart']");
await expect(page).toHaveScreenshot("dashboard.png", {
maxDiffPixelRatio: 0.02 // allow 2% pixel difference
});
});
Console error detection
test("page has no console errors", async ({ page }) => {
const errors: string[] = [];
page.on("console", msg => {
if (msg.type() === "error") errors.push(msg.text());
});
page.on("pageerror", err => errors.push(err.message));
await page.goto("/");
await page.waitForLoadState("networkidle");
expect(errors).toHaveLength(0);
});
Python Playwright
import pytest
from playwright.sync_api import Page, expect
def test_login(page: Page):
page.goto("/login")
page.wait_for_load_state("networkidle")
page.get_by_label("Email").fill("test@example.com")
page.get_by_label("Password").fill("password123")
page.get_by_role("button", name="Sign in").click()
page.wait_for_url("**/dashboard")
expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
def test_shows_error_on_invalid_login(page: Page):
page.goto("/login")
page.get_by_label("Email").fill("wrong@example.com")
page.get_by_label("Password").fill("wrong")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Invalid email or password")).to_be_visible()
# Run
pytest tests/ -v
# With headed browser (see what's happening)
pytest tests/ --headed
# Generate HTML report
pytest tests/ --html=report.html
Debugging Failures
// Slow down execution to watch what's happening
test.use({ actionTimeout: 5000 });
// Pause and inspect in headed mode
await page.pause(); // opens Playwright Inspector
// Log every network request
page.on("request", req => console.log("→", req.method(), req.url()));
page.on("response", res => console.log("←", res.status(), res.url()));
// Dump current DOM
const html = await page.content();
console.log(html.slice(0, 2000));
// Check what Playwright sees
await page.screenshot({ path: "debug.png", fullPage: true });
CI Integration
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
BASE_URL: http://localhost:3000
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Common Pitfalls
| Mistake | Why it fails | Fix | |---|---|---| | Not waiting for networkidle on dynamic apps | JS hasn't hydrated yet — selectors not found | await page.waitForLoadState("networkidle") | | Hardcoding CSS classes as selectors | Classes change on refactor | Use getByRole, getByLabel, getByText | | Not closing browser on error | Orphaned browser processes | Use test.afterEach cleanup or using | | page.click() before element is visible | Race condition | await page.getByRole(...).click() waits automatically | | Writing one giant test | Flaky, hard to debug, slow to isolate | One test per user journey | | No screenshot on failure | Can't debug without knowing what was on screen | Set screenshot: "only-on-failure" in config |
Definition of Done — Web App Testing
- [ ] Reconnaissance run first — selectors discovered from rendered DOM
- [ ]
getByRole,getByLabel, orgetByTextused — no raw CSS selectors - [ ] Each test covers exactly one user journey
- [ ]
waitForLoadState("networkidle")called after navigation - [ ] Screenshot on failure configured in
playwright.config.ts - [ ] Both happy path and error paths tested
- [ ] API mocking tested for failure states
- [ ] Tests are independent — no shared mutable state between tests
- [ ] Tests clean up after themselves (created data removed in
afterEach) - [ ] CI pipeline runs tests on every PR
- [ ] Failure artifacts (screenshots, traces) uploaded in CI
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: CODE-SAURABH
- Source: CODE-SAURABH/OpenSkills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.