agentleFS
aemerson-commitsaemerson-commits/shareable-skills/skills/webapp-testing/SKILL.md

webapp-testing

Skill1 starsChanged 8 months agoMITView on GitHub

Toolkit for interacting with and testing web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.

  • Reads credentials

196 other projects use a version of this, 168 of them edited.

Adopted by

Most recently taken up by

Used with

Projects with it also use academy-guide, skill-share and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • AnthropicSkill

    academy-guide

    Stop and check this skill before finishing any reply to a question about how to use Claude or a Claude product — it recommends matching courses, tutorials, and use cases from Claude Academy (academy.claude.com), Anthropic's learning hub. Trigger on: "how do I", "how can I", "getting started with", "what can Claude do", "teach me", "learn to use"; questions about artifacts, projects, skills, plugins, connectors, MCP; requests about rolling Claude out to a team, class, or organization; and any ask for training materials, onboarding content, or learning resources. Use it when the user is learning how to use a feature or product — not when they are mid-task and just want the task done. This skill composes with other skills: after consulting product documentation to answer how a Claude feature works, also check here for a matching course or tutorial — a docs-grounded answer and an Academy recommendation belong together. Only recommend on a strong match; never invent Academy content.

    Also in 8 projects that use this

  • ArogyaReddySkill

    skill-share

    A skill that creates new Claude skills and automatically shares them on Slack using Rube for seamless team collaboration and skill discovery.

    Also in 23 projects that use this

  • SKB3002Skill

    intelligent-routing

    Automatic agent selection and intelligent task routing. Analyzes user requests and automatically selects the best specialist agent(s) without requiring explicit user mentions.

    Also in 5 projects that use this

  • ArogyaReddySkill

    raffle-winner-picker

    Picks random winners from lists, spreadsheets, or Google Sheets for giveaways, raffles, and contests. Ensures fair, unbiased selection with transparency.

    Also in 26 projects that use this

  • zephyrwang6Skill

    topic-collector

    AI热点采集工具。从Twitter/X、Product Hunt、Reddit、Hacker News、博客等采集AI相关热点内容。当用户说"开始今日选题"、"采集热点"、"看看今天有什么新闻"、"今日AI热点"时触发。聚焦领域:Vibe Coding、Claude Skill、AI知识管理、AI模型更新、AI新产品、海外热点。

    Also in 4 projects that use this

  • zephyrwang6Skill

    deepl

    Übersetzt Texte, Dokumente und XLIFF-Dateien via DeepL API. Verwende für Übersetzungen mit /deepl translate, /deepl file oder /deepl xliff.

    Also in 4 projects that use this

  • +4 more from zephyrwang6
  • +1 more from ArogyaReddy

The file

Show its rules and text

We split this skill into the rules it gives an agent

22 rules. 1 is common, 8 are distinctive, and 13 are very specific to this project.

  • 1

    Common

    Most of the 190 edited versions we read keep these.

  • 8

    Distinctive

    This author's own choices. Worth a look.

  • 13

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Chrome cleanup safety: Never broad-scope-kill Chrome processes.

    line 11
  2. Very specific:

    If list_pages also fails, an orphaned Chrome holds the lock.

    line 23
  3. Very specific:

    Do NOT use new_page with isolatedContext for auth-gated pages — an isolated context carries no auth cookie and bounces to the login page.

    line 32
  4. Very specific:

    Write a temp script at /tmp/verify-<feature>.mjs:

    line 54
  5. Distinctive:

    Run the script with credentials injected from your secrets manager.

    line 67
  6. Very specific:

    Read the screenshot with the Read tool — visually verify.

    line 69
  7. Distinctive:

    NOTE: If you previously referenced a reusable helper script for this flow, write the cookie-exchange inline or store the snippet in a temp …

    line 150
  8. Very specific:

    Bootstrap admin short-circuit gotcha: If the app has a hardcoded list of admin emails in the frontend (e.g.

    line 211
  9. Distinctive:

    When hiding or renaming a UI label (e.g.

    line 249
  10. Very specific:

    Check any default-fallback code paths that humanize a route ID (e.g.

    line 253
  11. Very specific:

    Headed mode: Add headless: false when user wants to watch

    line 261
  12. Very specific:

    Screenshots directory: Use a consistent path like screenshots/

    line 263
  13. Distinctive:

    Write a throwaway script that imports the template function/module directly (no HTTP round-trip, no server needed).

    line 312
  14. Distinctive:

    Call the template function with representative and edge-case data to get the HTML string.

    line 313
  15. Distinctive:

    Read the screenshot, compare against the goal, iterate — edit the template, rerun, re-screenshot.

    line 316
  16. Distinctive:

    Delete the throwaway script when done, or keep it if it should become a repeatable check.

    line 317
  17. Very specific:

    Do wait for page.wait_for_load_state('networkidle') before inspection

    line 324
  18. Very specific:

    Use bundled scripts as black boxes: - Use --help to see usage, then invoke directly

    line 337
  19. Very specific:

    Use sync_playwright() for synchronous scripts

    line 338
  20. Common:

    Always close the browser when done

    line 339
  21. Very specific:

    Use descriptive selectors: text=, role=, CSS selectors, or IDs

    line 340
  22. Very specific:

    Add appropriate waits: page.wait_for_selector() or page.wait_for_timeout()

    line 341

“Common” is measured: kept by at least 6 in 10 of the 190 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Web Application Testing

Browser Session Safety (MANDATORY)

Chrome cleanup safety: Never broad-scope-kill Chrome processes. Only close pages you opened via a specific page ID. "Browser already running" errors from chrome-devtools MCP mean the MCP profile is in use — reuse it, open an isolated context, or ask the user to close that window.

chrome-devtools MCP — "browser already running" recovery

The chrome-devtools MCP server attaches to a persistent profile so auth SSO stays logged in. It does not close that browser at session end, so orphaned chrome.exe processes accumulate and keep the profile locked. A fresh session's first MCP call then fails with "browser already running ... Use --isolated", and list_pages / navigate_page may all fail until the server establishes its own connection — this is the recurring intermittent failure.

Prefer the Playwright temp-script for deployed-page verification. The headless loop below (throwaway context + auth cookie injection) has none of these failure modes — no persistent profile, no lock collision, no auth bounce. Use the chrome-devtools MCP only for interactive single-page poking, not routine post-deploy checks.

Recovery when the MCP IS needed and the lock is stuck:

  1. Try list_pages → select_page first — if the running browser belongs to this MCP server you can simply reuse it (no kill needed).
  2. If list_pages also fails, an orphaned Chrome holds the lock. Kill ONLY the MCP-profile Chrome processes (never broad-kill):
    Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" |
      Where-Object { $_.CommandLine -like '*chrome-devtools-mcp*' } |
      ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
    Then retry navigate_page. This filter matches only chrome-devtools-mcp processes — the user's real Chrome is untouched.
  3. Do NOT use new_page with isolatedContext for auth-gated pages — an isolated context carries no auth cookie and bounces to the login page. Isolated contexts only work for public URLs.
  4. A secondary MCP server using a separate profile is not auth-logged-in by default — only a real fallback after a one-time manual login in that profile.

Permanent prevention (optional): a SessionEnd hook that runs the kill command above would stop orphans from accumulating between sessions. Not yet wired — ask the user before adding it to settings.json.

To test web applications, write native Playwright scripts (Node.js or Python, depending on your environment).

Helper Scripts Available:

  • scripts/cf-session.py - Legacy persistent sessions (cookie-based, requires manual login)
  • scripts/with_server.py - Manages server lifecycle (supports multiple servers)

Always run scripts with --help first to see usage. DO NOT read the source until you try running the script first and find that a customized solution is absolutely necessary. These scripts can be very large and thus pollute your context window. They exist to be called directly as black-box scripts rather than ingested into your context window.

Temp Playwright Script Iteration Loop (canonical)

For any visual UI verification, the reliable loop is:

  1. Write a temp script at /tmp/verify-<feature>.mjs:
    import { chromium } from 'playwright';
    const browser = await chromium.launch({ headless: true });
    const ctx = await browser.newContext();
    // CF Access service-token cookie injection (required for CF Access-protected apps)
    await ctx.addCookies([{ name: 'CF_Authorization', value: process.env.CF_ACCESS_TOKEN, domain: '{{your-app}}.example.com', path: '/' }]);
    const page = await ctx.newPage();
    await page.goto('https://{{your-app}}.example.com/dashboard');
    await page.screenshot({ path: '/tmp/screenshots/verify-1.png', fullPage: true });
    await browser.close();
  2. Run the script with credentials injected from your secrets manager.
  3. Read the screenshot with the Read tool — visually verify.
  4. Edit the script (add selectors, clicks, state capture) — re-run — re-read screenshot.

This beats the MCP chrome-devtools flow for scripted multi-step verification because the temp script is version-controllable (paste into the session notes) and re-runnable in the background. CF Access cookie injection (exchange service-token headers for CF_Authorization cookie via ctx.addCookies()) is required for CF-Access-protected apps — this beats the MCP because cookies are injected pre-navigation.

For CSS/layout work: Read screenshot → Grep CSS class usage → Edit → npm run build → re-screenshot → repeat (2–3 rounds typical). The same Write→Screenshot→Read→iterate cycle applies to email-HTML composition (generate HTML draft → screenshot via Playwright → iterate on the template).

See references/iteration-loops.md for the full temp-script template, CF Access cookie injection detail, CSS debug loop, email-HTML composition loop, and minified deployed-bundle grep technique.

Decision Tree: Choosing Your Approach

User task → Is the target behind CF Access (dev/prod sites)?
    ├─ Yes → Use CF Access Service Token (PREFERRED — no manual login needed)
    │         1. Set extraHTTPHeaders with CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET
    │         2. Navigate freely — all requests auto-authenticate
    │         3. See "CF Access Bypass" section below
    │
    └─ No → Is it behind another auth (SSO, API key, etc.)?
        ├─ Yes → Use service token / API key headers (PREFERRED)
        │         See "Service Token Authentication" below
        │
        └─ No → Is it static HTML?
            ├─ Yes → Read HTML file directly to identify selectors
            │         ├─ Success → Write Playwright script using selectors
            │         └─ Fails/Incomplete → Treat as dynamic (below)
            │
            └─ No (dynamic webapp) → Is the server already running?
                ├─ No → Run: python scripts/with_server.py --help
                │        Then use the helper + write simplified Playwright script
                │
                └─ 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

CF Access Bypass — Choose Your Pattern

Two working patterns. Pick based on what you're doing:

PatternBest forWhy
CF_Authorization cookie injection (simplest)Visual verification, multi-page flows, SPAsCookie fires on every request including SPA fetch() — no route intercepts needed
extraHTTPHeaders + route interceptsFull admin impersonation (identity, groups, admin flag)Lets you mock identity + auth endpoints for admin-only UI

Pattern A — Cookie Injection (PREFERRED for visual checks)

Under the hood: call the target URL with CF-Access-Client-Id + CF-Access-Client-Secret headers, extract the CF_Authorization JWT from the Set-Cookie response header, then inject it into a Playwright context before the first navigation. Subsequent fetch() calls in the SPA automatically include the cookie.

const { chromium } = require('playwright')

// Exchange service-token headers for CF_Authorization cookie
const CF_ID = process.env.CF_ACCESS_CLIENT_ID
const CF_SECRET = process.env.CF_ACCESS_CLIENT_SECRET

const res = await fetch('https://{{your-app}}.example.com/', {
  headers: { 'CF-Access-Client-Id': CF_ID, 'CF-Access-Client-Secret': CF_SECRET },
  redirect: 'manual',
})
const setCookie = res.headers.get('set-cookie') || ''
const cfAuth = /CF_Authorization=([^;]+)/.exec(setCookie)?.[1]

const browser = await chromium.launch({ headless: true })
const context = await browser.newContext()
await context.addCookies([{ name: 'CF_Authorization', value: cfAuth, domain: '{{your-app}}.example.com', path: '/' }])
const page = await context.newPage()
await page.goto('https://{{your-app}}.example.com/admin')

Why this beats chrome-devtools MCP: The MCP can't set cookies before the first request, so CF Access redirects to cloudflareaccess.com before any JS runs. Playwright's context.addCookies() is pre-navigation, so the browser sends CF_Authorization on the first GET.

NOTE: If you previously referenced a reusable helper script for this flow, write the cookie-exchange inline or store the snippet in a temp file for one-off runs.

Pattern B — extraHTTPHeaders + Route Intercepts

For all CF Access-protected sites, use the service token. This requires no manual login — set headers on the browser context and all requests authenticate automatically.

Server-side ALLOW_SERVICE_TOKEN_AUTH: On dev projects, an env var like ALLOW_SERVICE_TOKEN_AUTH=true lets the auth middleware trust a CF-Access-Authenticated-User-Email header without a real JWT. Set only on dev — never on production.

Credentials: Load from your secrets manager (environment variables, encrypted .env, etc.). Never hardcode credentials in scripts or skills.

Credential Loading

# Shell — load from environment
CF_ID=$CF_ACCESS_CLIENT_ID
CF_SECRET=$CF_ACCESS_CLIENT_SECRET
// Node.js — read from process.env (set before running)
const CF_HEADERS = {
  'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID,
  'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET,
  'CF-Access-Authenticated-User-Email': 'test-user@example.com',
};

Basic Usage (Node.js CJS)

const { chromium } = require('playwright');

const CF_HEADERS = {
  'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID,
  'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET,
  'CF-Access-Authenticated-User-Email': 'test-user@example.com',
};

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  extraHTTPHeaders: CF_HEADERS,
  viewport: { width: 1920, height: 1080 },
});
const page = await context.newPage();
await page.goto('https://{{your-app}}.example.com/', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
await page.screenshot({ path: 'screenshots/app.png' });
await browser.close();

Full Admin Access (Route Intercepts + Header Injection)

Service token extraHTTPHeaders only apply to page navigation, NOT to SPA fetch() calls. To get full admin access with live API data, you need two layers:

  1. Specific intercepts (register FIRST) — mock identity, group membership, admin status, tab visibility
  2. Catch-all header injection (register LAST) — inject service token headers into ALL fetch() calls

CRITICAL: Playwright routes match LIFO (last registered first). The catch-all must use route.fallback() for URLs handled by specific intercepts, otherwise it steals the request.

Finding the real isAdmin source: Many SPAs have a consolidated auth-init endpoint (e.g. /api/auth-init) that returns role flags in one payload. Intercept this rather than individual check endpoints to reliably flip admin status for RBAC UI tests.

Bootstrap admin short-circuit gotcha: If the app has a hardcoded list of admin emails in the frontend (e.g. ADMIN_EMAILS = ['superuser@example.com']), the app may grant admin even when your /api/auth-init intercept returns isAdmin: false. Use a non-bootstrap test email (e.g. non-admin-test@example.com) to test the non-admin UI path.

const TEST_EMAIL = asAdmin ? 'admin@example.com' : 'non-admin-test@example.com';

// --- Register SPECIFIC intercepts FIRST ---
// Real source of isAdmin / role flags (adapt endpoint to your app).
await page.route('**/api/auth-init', route => {
  route.fulfill({ status: 200, contentType: 'application/json',
    body: JSON.stringify({ email: TEST_EMAIL, isAdmin: asAdmin, roles: [] }) });
});
await page.route('**/cdn-cgi/access/get-identity', route => {
  route.fulfill({ status: 200, contentType: 'application/json',
    body: JSON.stringify({ email: TEST_EMAIL, name: asAdmin ? 'Admin User' : 'Non Admin' }) });
});
await page.route('**/api/check-group*', route => {
  route.fulfill({ status: 200, contentType: 'application/json',
    body: JSON.stringify({ isMember: asAdmin }) });
});

// --- Register CATCH-ALL LAST ---
await page.route('**/api/**', async (route) => {
  const url = route.request().url();
  if (url.includes('/api/auth-init') || url.includes('/api/check-group')) {
    await route.fallback(); // Delegate to specific intercept above
    return;
  }
  const headers = { ...route.request().headers(),
    'CF-Access-Client-Id': CF_HEADERS['CF-Access-Client-Id'],
    'CF-Access-Client-Secret': CF_HEADERS['CF-Access-Client-Secret'],
    'CF-Access-Authenticated-User-Email': TEST_EMAIL,
  };
  await route.continue({ headers });
});

UI Label Audit — Visual Verification (MANDATORY)

When hiding or renaming a UI label (e.g. "Admin" for non-admin users), the same text is often rendered in multiple places: sidebar, main header, document title, tab labels, tooltip titles, breadcrumbs. Fixing one site and shipping leaves the others leaking.

Before claiming a label change is done:

  1. grep -rn '"YourLabel"' src/ — find ALL render sites
  2. Check any default-fallback code paths that humanize a route ID (e.g. activeView.charAt(0).toUpperCase() + activeView.slice(1) turns admin → "Admin" for free, bypassing any lookup map)
  3. Visual verification is mandatory — HTTP 200 and unit tests don't prove what the user sees. Take a Playwright screenshot of the actual rendered page.

Symptom of this gotcha: your RBAC intercept returns isAdmin: false, the JS unit test passes, but a main page header still renders "Admin" through a default fallback path that wasn't gated.

Key Notes

  • Use domcontentloaded not networkidle — React SPAs keep connections alive
  • Wait 3-5s after navigation for data-driven content; 8-10s for heavy views
  • Headed mode: Add headless: false when user wants to watch
  • Auth failure signal: HTTP 307 redirect = service token not accepted
  • Screenshots directory: Use a consistent path like screenshots/

Example: Using with_server.py

To start a server, run --help first, then use the helper:

Single server:

python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py

Multiple servers (e.g., backend + frontend):

python scripts/with_server.py \
  --server "cd backend && python server.py" --port 3000 \
  --server "cd frontend && npm run dev" --port 5173 \
  -- python your_automation.py

To create an automation script, include only Playwright logic (servers are managed automatically):

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode
    page = browser.new_page()
    page.goto('http://localhost:5173') # Server already running and ready
    page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
    # ... your automation logic
    browser.close()

Reconnaissance-Then-Action Pattern

  1. Inspect rendered DOM:
    page.screenshot(path='/tmp/inspect.png', full_page=True)
    content = page.content()
    page.locator('button').all()
  2. Identify selectors from inspection results
  3. Execute actions using discovered selectors

Server-Generated HTML Template → PNG Verification

For a report/document/email HTML template that's rendered server-side with no live URL to navigate to, don't reason about layout from the template source — render it and look:

  1. Write a throwaway script that imports the template function/module directly (no HTTP round-trip, no server needed).
  2. Call the template function with representative and edge-case data to get the HTML string.
  3. page.setContent(html) in a headless Playwright page.
  4. Screenshot to a scratch path.
  5. Read the screenshot, compare against the goal, iterate — edit the template, rerun, re-screenshot.
  6. Delete the throwaway script when done, or keep it if it should become a repeatable check.

A computed or predicted layout is never visual proof — bounding-box math or "the HTML string contains the right elements" doesn't establish that a report, invoice, or email actually renders correctly at a usable size. Only a rendered screenshot does.

Common Pitfall

  • Don't inspect the DOM before waiting for networkidle on dynamic apps
  • Do wait for page.wait_for_load_state('networkidle') before inspection

Screenshot Timing

Wait 1-2 seconds after:

  • Starting a dev server
  • Navigating to a new page
  • Theme transitions

before taking screenshots. Without this pause, captures may show loading states or incomplete renders.

Best Practices

  • Use bundled scripts as black boxes - Use --help to see usage, then invoke directly
  • Use sync_playwright() for synchronous scripts
  • Always close the browser when done
  • Use descriptive selectors: text=, role=, CSS selectors, or IDs
  • Add appropriate waits: page.wait_for_selector() or page.wait_for_timeout()

Reference Files

  • examples/ - Examples showing common patterns:
    • element_discovery.py - Discovering buttons, links, and inputs on a page
    • static_html_automation.py - Using file:// URLs for local HTML
    • console_logging.py - Capturing console logs during automation

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.