seed-skills/webapp-testing/SKILL.md
Anthropic's official web application testing skill using native Python Playwright scripts with helper utilities for server lifecycle management, browser automation, and comprehensive E2E testing workflows.
npx skillsauth add PramodDutta/qaskills WebApp Testing with PlaywrightInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert QA automation engineer using Anthropic's official webapp-testing skill. This skill specializes in Python-based Playwright testing with integrated server lifecycle management. When the user asks you to test web applications, write E2E tests, or manage test environments, follow these detailed instructions.
# Install Playwright for Python
pip install playwright pytest-playwright
# Install Playwright browsers
playwright install
# Install with additional dependencies
pip install playwright pytest-playwright pytest-asyncio faker
tests/
webapp/
test_authentication.py
test_dashboard.py
test_checkout.py
test_api_integration.py
fixtures/
server_fixture.py
browser_fixture.py
data_fixture.py
helpers/
server_manager.py
browser_utils.py
test_data.py
pages/
login_page.py
dashboard_page.py
base_page.py
config/
test_config.py
server_config.py
pytest.ini
conftest.py
# helpers/server_manager.py
import subprocess
import time
import requests
from typing import Optional
import signal
import sys
class ServerManager:
"""Manage application server lifecycle for testing."""
def __init__(
self,
command: str,
port: int = 3000,
host: str = "localhost",
startup_timeout: int = 30,
health_check_path: str = "/",
):
self.command = command
self.port = port
self.host = host
self.startup_timeout = startup_timeout
self.health_check_path = health_check_path
self.process: Optional[subprocess.Popen] = None
self.base_url = f"http://{host}:{port}"
def start(self) -> None:
"""Start the application server."""
print(f"Starting server: {self.command}")
self.process = subprocess.Popen(
self.command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=lambda: signal.signal(signal.SIGINT, signal.SIG_IGN)
)
# Wait for server to be ready
self._wait_for_server()
print(f"Server running at {self.base_url}")
def _wait_for_server(self) -> None:
"""Wait for server to respond to health checks."""
start_time = time.time()
health_url = f"{self.base_url}{self.health_check_path}"
while time.time() - start_time < self.startup_timeout:
try:
response = requests.get(health_url, timeout=1)
if response.status_code < 500:
print(f"Server ready (status: {response.status_code})")
return
except requests.exceptions.RequestException:
pass
time.sleep(0.5)
raise TimeoutError(
f"Server did not start within {self.startup_timeout} seconds"
)
def stop(self) -> None:
"""Stop the application server."""
if self.process:
print("Stopping server...")
self.process.terminate()
try:
self.process.wait(timeout=10)
except subprocess.TimeoutExpired:
print("Server did not stop gracefully, forcing...")
self.process.kill()
self.process.wait()
self.process = None
print("Server stopped")
def restart(self) -> None:
"""Restart the server."""
self.stop()
time.sleep(1)
self.start()
def is_running(self) -> bool:
"""Check if server is running."""
if not self.process:
return False
try:
response = requests.get(
f"{self.base_url}{self.health_check_path}",
timeout=1
)
return response.status_code < 500
except requests.exceptions.RequestException:
return False
def __enter__(self):
"""Context manager entry."""
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.stop()
# conftest.py
import pytest
from helpers.server_manager import ServerManager
@pytest.fixture(scope="session")
def server():
"""Start application server for test session."""
server_manager = ServerManager(
command="npm run dev",
port=3000,
startup_timeout=60,
health_check_path="/api/health"
)
server_manager.start()
yield server_manager
server_manager.stop()
@pytest.fixture(scope="session")
def base_url(server):
"""Provide base URL for tests."""
return server.base_url
@pytest.fixture(scope="function")
def page(browser, base_url):
"""Create new page for each test."""
context = browser.new_context(base_url=base_url)
page = context.new_page()
yield page
context.close()
# pages/base_page.py
from playwright.sync_api import Page, expect
from typing import Optional
class BasePage:
"""Base class for all page objects."""
def __init__(self, page: Page, base_url: str):
self.page = page
self.base_url = base_url
def navigate(self, path: str = "") -> None:
"""Navigate to a specific path."""
url = f"{self.base_url}{path}"
self.page.goto(url)
def wait_for_load(self, state: str = "networkidle") -> None:
"""Wait for page to load."""
self.page.wait_for_load_state(state)
def get_title(self) -> str:
"""Get page title."""
return self.page.title()
def take_screenshot(self, filename: str, full_page: bool = True) -> None:
"""Take screenshot of page."""
self.page.screenshot(path=filename, full_page=full_page)
def wait_for_selector(
self,
selector: str,
state: str = "visible",
timeout: int = 30000
) -> None:
"""Wait for element to be in specific state."""
self.page.wait_for_selector(selector, state=state, timeout=timeout)
def execute_script(self, script: str, *args):
"""Execute JavaScript in page context."""
return self.page.evaluate(script, *args)
def reload(self) -> None:
"""Reload the page."""
self.page.reload()
def go_back(self) -> None:
"""Navigate back."""
self.page.go_back()
def go_forward(self) -> None:
"""Navigate forward."""
self.page.go_forward()
# pages/login_page.py
from playwright.sync_api import Page, expect
from pages.base_page import BasePage
class LoginPage(BasePage):
"""Login page object."""
def __init__(self, page: Page, base_url: str):
super().__init__(page, base_url)
# Locators
self.email_input = page.get_by_label("Email")
self.password_input = page.get_by_label("Password")
self.submit_button = page.get_by_role("button", name="Sign in")
self.error_message = page.get_by_role("alert")
self.forgot_password_link = page.get_by_role("link", name="Forgot password?")
self.remember_me_checkbox = page.get_by_label("Remember me")
def goto(self) -> None:
"""Navigate to login page."""
self.navigate("/login")
def login(self, email: str, password: str, remember_me: bool = False) -> None:
"""Perform login action."""
self.email_input.fill(email)
self.password_input.fill(password)
if remember_me:
self.remember_me_checkbox.check()
self.submit_button.click()
def expect_error(self, message: str) -> None:
"""Verify error message is displayed."""
expect(self.error_message).to_be_visible()
expect(self.error_message).to_have_text(message)
def expect_logged_in(self) -> None:
"""Verify successful login."""
expect(self.page).to_have_url("/dashboard")
def click_forgot_password(self) -> None:
"""Click forgot password link."""
self.forgot_password_link.click()
expect(self.page).to_have_url("/forgot-password")
# pages/dashboard_page.py
from playwright.sync_api import Page, expect
from pages.base_page import BasePage
from typing import List, Dict
class DashboardPage(BasePage):
"""Dashboard page object."""
def __init__(self, page: Page, base_url: str):
super().__init__(page, base_url)
# Locators
self.welcome_heading = page.get_by_role("heading", name="Welcome")
self.user_menu = page.get_by_test_id("user-menu")
self.logout_button = page.get_by_role("button", name="Logout")
self.stats_cards = page.locator(".stat-card")
self.activity_feed = page.get_by_test_id("activity-feed")
def goto(self) -> None:
"""Navigate to dashboard."""
self.navigate("/dashboard")
def expect_welcome_message(self, username: str) -> None:
"""Verify welcome message contains username."""
expect(self.welcome_heading).to_contain_text(username)
def get_stat_value(self, stat_name: str) -> str:
"""Get value for specific stat."""
stat_card = self.page.locator(f".stat-card[data-stat='{stat_name}']")
return stat_card.locator(".stat-value").text_content()
def get_all_stats(self) -> Dict[str, str]:
"""Get all dashboard statistics."""
stats = {}
count = self.stats_cards.count()
for i in range(count):
card = self.stats_cards.nth(i)
name = card.get_attribute("data-stat")
value = card.locator(".stat-value").text_content()
stats[name] = value
return stats
def logout(self) -> None:
"""Logout user."""
self.user_menu.click()
self.logout_button.click()
expect(self.page).to_have_url("/login")
def get_recent_activities(self, limit: int = 5) -> List[Dict[str, str]]:
"""Get recent activity items."""
activities = []
items = self.activity_feed.locator(".activity-item").all()[:limit]
for item in items:
activities.append({
"title": item.locator(".activity-title").text_content(),
"time": item.locator(".activity-time").text_content(),
"type": item.get_attribute("data-type")
})
return activities
# tests/webapp/test_authentication.py
import pytest
from playwright.sync_api import Page, expect
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage
class TestAuthentication:
"""Authentication test suite."""
@pytest.fixture(autouse=True)
def setup(self, page: Page, base_url: str):
"""Setup for each test."""
self.login_page = LoginPage(page, base_url)
self.dashboard_page = DashboardPage(page, base_url)
def test_successful_login(self):
"""Test successful login with valid credentials."""
self.login_page.goto()
self.login_page.login("[email protected]", "SecurePass123!")
self.login_page.expect_logged_in()
self.dashboard_page.expect_welcome_message("User")
def test_login_with_invalid_password(self):
"""Test login fails with incorrect password."""
self.login_page.goto()
self.login_page.login("[email protected]", "wrongpassword")
self.login_page.expect_error("Invalid email or password")
def test_login_with_nonexistent_email(self):
"""Test login fails with non-registered email."""
self.login_page.goto()
self.login_page.login("[email protected]", "SomePass123!")
self.login_page.expect_error("Invalid email or password")
@pytest.mark.parametrize("email,password,error", [
("", "password", "Email is required"),
("[email protected]", "", "Password is required"),
("invalid-email", "password", "Please enter a valid email"),
("[email protected]", "short", "Password must be at least 8 characters"),
])
def test_login_validation(self, email: str, password: str, error: str):
"""Test form validation errors."""
self.login_page.goto()
self.login_page.email_input.fill(email)
self.login_page.password_input.fill(password)
self.login_page.submit_button.click()
self.login_page.expect_error(error)
def test_remember_me_functionality(self, page: Page):
"""Test remember me checkbox persists session."""
self.login_page.goto()
self.login_page.login("[email protected]", "SecurePass123!", remember_me=True)
self.dashboard_page.expect_welcome_message("User")
# Close and reopen browser
page.context().close()
new_context = page.context().browser.new_context()
new_page = new_context.new_page()
# Should still be logged in
new_page.goto(f"{self.login_page.base_url}/dashboard")
expect(new_page).to_have_url("/dashboard")
new_context.close()
# tests/webapp/test_dashboard.py
import pytest
from playwright.sync_api import Page, expect
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage
class TestDashboard:
"""Dashboard functionality tests."""
@pytest.fixture(autouse=True)
def setup(self, page: Page, base_url: str):
"""Setup authenticated session."""
self.login_page = LoginPage(page, base_url)
self.dashboard_page = DashboardPage(page, base_url)
# Login before each test
self.login_page.goto()
self.login_page.login("[email protected]", "SecurePass123!")
def test_dashboard_loads_all_components(self):
"""Test all dashboard components are visible."""
self.dashboard_page.goto()
expect(self.dashboard_page.welcome_heading).to_be_visible()
expect(self.dashboard_page.stats_cards).to_have_count(4)
expect(self.dashboard_page.activity_feed).to_be_visible()
def test_dashboard_statistics_accuracy(self):
"""Test dashboard stats match expected values."""
self.dashboard_page.goto()
stats = self.dashboard_page.get_all_stats()
assert "users" in stats
assert "revenue" in stats
assert "orders" in stats
assert "growth" in stats
# Verify stat values are numeric
assert stats["users"].isdigit()
assert "$" in stats["revenue"]
def test_activity_feed_updates(self, page: Page):
"""Test activity feed shows recent actions."""
self.dashboard_page.goto()
initial_activities = self.dashboard_page.get_recent_activities()
initial_count = len(initial_activities)
# Perform an action that generates activity
page.get_by_role("button", name="Create New").click()
page.get_by_label("Title").fill("Test Item")
page.get_by_role("button", name="Save").click()
# Wait for activity feed to update
page.wait_for_timeout(1000)
self.dashboard_page.reload()
updated_activities = self.dashboard_page.get_recent_activities()
assert len(updated_activities) > initial_count
assert updated_activities[0]["title"] == "Created Test Item"
def test_logout_functionality(self):
"""Test user can logout successfully."""
self.dashboard_page.goto()
self.dashboard_page.logout()
expect(self.login_page.page).to_have_url("/login")
# Verify cannot access dashboard after logout
self.dashboard_page.goto()
expect(self.login_page.page).to_have_url("/login")
# helpers/test_data.py
from faker import Faker
from typing import Dict, List
import random
fake = Faker()
class TestDataGenerator:
"""Generate realistic test data."""
@staticmethod
def user(
email: str = None,
password: str = "SecurePass123!",
**kwargs
) -> Dict[str, str]:
"""Generate user data."""
return {
"email": email or fake.email(),
"password": password,
"firstName": kwargs.get("firstName", fake.first_name()),
"lastName": kwargs.get("lastName", fake.last_name()),
"phone": kwargs.get("phone", fake.phone_number()),
"address": kwargs.get("address", fake.address()),
}
@staticmethod
def product(**kwargs) -> Dict:
"""Generate product data."""
return {
"name": kwargs.get("name", fake.catch_phrase()),
"description": kwargs.get("description", fake.text(max_nb_chars=200)),
"price": kwargs.get("price", round(random.uniform(10, 1000), 2)),
"category": kwargs.get("category", random.choice(["Electronics", "Clothing", "Books", "Home"])),
"stock": kwargs.get("stock", random.randint(0, 100)),
}
@staticmethod
def order(user_id: str, products: List[Dict]) -> Dict:
"""Generate order data."""
return {
"userId": user_id,
"products": products,
"total": sum(p["price"] * p["quantity"] for p in products),
"status": random.choice(["pending", "processing", "shipped", "delivered"]),
"createdAt": fake.date_time_this_month().isoformat(),
}
# tests/webapp/test_checkout.py
import pytest
from helpers.test_data import TestDataGenerator
class TestCheckout:
"""Checkout flow tests."""
@pytest.fixture
def test_user(self):
"""Generate test user."""
return TestDataGenerator.user()
@pytest.fixture
def test_products(self):
"""Generate test products."""
return [
TestDataGenerator.product(name="Widget A", price=29.99),
TestDataGenerator.product(name="Widget B", price=49.99),
]
def test_checkout_flow(self, page, base_url, test_user, test_products):
"""Test complete checkout flow."""
# Navigate and login
page.goto(f"{base_url}/login")
page.get_by_label("Email").fill(test_user["email"])
page.get_by_label("Password").fill(test_user["password"])
page.get_by_role("button", name="Sign in").click()
# Add products to cart
for product in test_products:
page.goto(f"{base_url}/products")
page.get_by_text(product["name"]).click()
page.get_by_role("button", name="Add to Cart").click()
# Proceed to checkout
page.get_by_test_id("cart-icon").click()
expect(page.get_by_test_id("cart-items")).to_have_count(len(test_products))
page.get_by_role("button", name="Checkout").click()
# Fill shipping information
page.get_by_label("Name").fill(f"{test_user['firstName']} {test_user['lastName']}")
page.get_by_label("Address").fill(test_user["address"])
page.get_by_label("Phone").fill(test_user["phone"])
# Complete order
page.get_by_role("button", name="Place Order").click()
# Verify success
expect(page.get_by_text("Order confirmed")).to_be_visible()
expect(page).to_have_url("/orders/confirmation")
# conftest.py
import pytest
from helpers.server_manager import ServerManager
@pytest.fixture(scope="session", params=["development", "production"])
def server(request):
"""Run tests against different server configurations."""
config = {
"development": {
"command": "npm run dev",
"port": 3000,
},
"production": {
"command": "npm run build && npm start",
"port": 8080,
}
}
cfg = config[request.param]
server_manager = ServerManager(
command=cfg["command"],
port=cfg["port"]
)
server_manager.start()
yield server_manager
server_manager.stop()
# helpers/server_manager.py (extended)
class ServerManager:
# ... previous methods ...
def seed_database(self, seed_script: str) -> None:
"""Run database seed script."""
print("Seeding database...")
result = subprocess.run(
seed_script,
shell=True,
capture_output=True,
text=True
)
if result.returncode != 0:
raise RuntimeError(f"Database seeding failed: {result.stderr}")
print("Database seeded successfully")
def reset_database(self, reset_script: str) -> None:
"""Reset database to clean state."""
print("Resetting database...")
subprocess.run(reset_script, shell=True, check=True)
print("Database reset complete")
# conftest.py
@pytest.fixture(scope="function")
def clean_database(server):
"""Provide clean database for each test."""
server.reset_database("npm run db:reset")
server.seed_database("npm run db:seed")
yield
This skill provides a comprehensive foundation for Python-based web application testing with Playwright, featuring integrated server lifecycle management and helper utilities optimized for Anthropic's testing workflows.
development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.