plugins/v1tamins/skills/v1-e2e-testing/SKILL.md
Use when implementing E2E tests, debugging flaky tests, testing web applications with Playwright, or establishing E2E testing standards. Triggers on "e2e test", "end-to-end", "Playwright", "flaky test", "browser test".
npx skillsauth add v1-io/v1tamins v1-e2e-testingInstall 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.
Build reliable, fast E2E test suites that catch regressions and enable confident deployments.
Test a local web application:
# If server not running, resolve the bundled helper first
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
for dir in \
"$REPO_ROOT/plugins/v1tamins/skills/v1-e2e-testing" \
"${CLAUDE_PLUGIN_ROOT:-}" \
"$HOME/.codex/skills/v1-e2e-testing" \
"$HOME/.claude/skills/v1-e2e-testing"; do
[ -n "$dir" ] && [ -f "$dir/scripts/with_server.py" ] && SKILL_ROOT="$dir" && break
done
if [ -z "${SKILL_ROOT:-}" ]; then
echo "ERROR: Could not find scripts/with_server.py" >&2
exit 1
fi
python3 "$SKILL_ROOT/scripts/with_server.py" --server "npm run dev" --port 3000 -- python your_test.py
# If server already running, write Playwright directly
Basic Playwright test:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:3000')
page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS
# Reconnaissance first
page.screenshot(path='/tmp/inspect.png', full_page=True)
# Then actions
page.get_by_role('button', name='Login').click()
browser.close()
Good for:
Not for:
User task → Is it static HTML?
├─ Yes → Read HTML file directly for selectors
│ └─ Write Playwright script using selectors
│
└─ No (dynamic webapp) → Is the server already running?
├─ No → Use scripts/with_server.py
│
└─ Yes → Reconnaissance-then-action:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectors
When using Playwright as a debug loop, make the script prove the specific symptom the user reported:
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
timeout: 30000,
expect: { timeout: 5000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["html"], ["github"]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
],
});
Encapsulate page logic in classes:
// pages/LoginPage.ts
import { Page, Locator } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.loginButton = page.getByRole("button", { name: "Login" });
}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
// Usage in tests
test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("[email protected]", "password123");
await expect(page).toHaveURL("/dashboard");
});
page.getByRole('button', { name: 'Submit' })page.getByLabel('Email')page.getByPlaceholder('Search...')page.getByTestId('submit-button')page.getByText('Welcome')page.locator('.btn-primary') - brittle!Multi-candidate strategy for Auth0/iframes:
// Frame-aware selectors
const frame = page.frameLocator('[data-testid="auth0-frame"]');
await frame.getByRole('textbox', { name: 'Email' }).fill(email);
// BAD: Fixed timeouts
await page.waitForTimeout(3000); // Flaky!
// GOOD: Wait for specific conditions
await page.waitForLoadState("networkidle");
await page.waitForURL("/dashboard");
// BETTER: Auto-waiting with assertions
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page.getByRole("button")).toBeEnabled();
// Wait for API response
const responsePromise = page.waitForResponse(
(res) => res.url().includes("/api/users") && res.status() === 200
);
await page.getByRole("button", { name: "Load" }).click();
await responsePromise;
Storage state for pre-auth (recommended):
// global-setup.ts
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.TEST_USER!);
await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!);
await page.getByRole("button", { name: "Login" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: "auth.json" });
await browser.close();
}
// playwright.config.ts
use: {
storageState: "auth.json",
}
# Headed mode
npx playwright test --headed
# Debug mode (step through)
npx playwright test --debug
# Pause in test
await page.pause(); # Opens inspector
# Trace viewer
npx playwright show-trace trace.zip
Add test steps for reporting:
test('checkout flow', async ({ page }) => {
await test.step('Add item to cart', async () => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
});
await test.step('Proceed to checkout', async () => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
});
});
| Cause | Fix | |-------|-----| | Fixed timeouts | Use proper waits (networkidle, assertions) | | Race conditions | Wait for specific state before acting | | Test interdependence | Make tests independent, clean up data | | Stale selectors | Use role-based selectors, avoid CSS classes | | Animation interference | Wait for animations, disable in test mode |
Root cause checklist:
waitForTimeout() anywhere? Replace with proper waitsscripts/with_server.py - Server lifecycle management
if [ -z "${SKILL_ROOT:-}" ]; then
echo "ERROR: Could not find scripts/with_server.py" >&2
exit 1
fi
# Single server
python3 "$SKILL_ROOT/scripts/with_server.py" --server "npm run dev" --port 3000 -- python test.py
# Multiple servers (backend + frontend)
python3 "$SKILL_ROOT/scripts/with_server.py" \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python test.py
Run python3 "$SKILL_ROOT/scripts/with_server.py" --help first.
.btn.btn-primary, use rolesFor advanced Playwright patterns, see:
development
Run an extremely strict maintainability review for abstraction quality, giant files, and spaghetti-condition growth. Use for large prs, new features/architectures, a deep code quality audit, or especially harsh maintainability review.
testing
Commit, push, open, and land a pull request through CI handoff. Use when work is complete and the user wants an agent to create or update a PR, open it as a draft, monitor GitHub checks with `gh pr checks`, fix failed checks, retry up to three remediation pushes, mark the PR ready for review once green, and move a linked Linear ticket to Human Review when one exists. Trigger on requests like 'land this PR', 'open and monitor a PR', 'commit push and watch CI', 'get this ready for review', or 'finish the PR workflow'.
development
Use when reviewing a PR, reviewing the current branch, or posting code review feedback to GitHub. Triggers on "review this PR", "code review", "check this pull request", "review my branch", "review and fix".
development
Use when a plan, PRD, proposal, or implementation outline is overscoped, too ambitious for the immediate goal, or needs to be reduced to a bare-bones version. Triggers on "bare bones", "no damn whistles", "no bells and whistles", "strip this plan", "trim this plan", "scope creep", "descope this plan", "MVP only".