schalkneethling/masonry-gridlanes-wc · Archived

playwright-best-practices

Comprehensive playwright testing best practices including ARIA snapshot testing for structural validation.

First seen May 18, 2026

Installation

$ npx skills add schalkneethling/masonry-gridlanes-wc --skill playwright-best-practices

Summary

  • Comprehensive playwright testing best practices including ARIA snapshot testing for structural validation.
  • Use when asked to "write playwright tests", "validate page structure", "test accessibility", "improve playwright test quality", "review playwright test code", or "advise on playwright test architecture".

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from schalkneethling/masonry-gridlanes-wc · top by installs.

npx skills add schalkneethling/masonry-gridlanes-wc

Browse all from schalkneethling/masonry-gridlanes-wc

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 11
License LICENSE
Default branch main
Open issues 14
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

LicenseMIT

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 28,658 B
  • docs SUMMARY.md 343 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

Playwright Testing Best Practices

Description

This skill teaches WHAT makes good tests and WHY certain patterns prevent failures. Provides the decision-making framework behind test structure, locator selection, assertion strategies, and debugging approaches. Works in conjunction with playwright-cli skill (which teaches HOW to execute commands). Apply when planning test strategy, choosing between ARIA snapshots vs individual locators, structuring assertions, debugging test failures, reviewing test code, or advising on test architecture. Emphasizes ARIA snapshot testing for structural validation, user-visible behavior testing, locator resilience hierarchy, web-first assertion patterns, test isolation principles, and production-proven debugging strategies.

Core Testing Philosophy

Test User-Visible Behavior

Principle: Tests should verify what end users experience, not implementation details.

Rules:

  • Focus on rendered output that users can see and interact with
  • Avoid testing internal function names, data structures, or CSS class names
  • Test the same interface that users experience
  • Don't assert on implementation details that could change without affecting user experience

Anti-patterns:

// ❌ Testing implementation details
expect(component.internalState.items).toBeArray();
expect(element.className).toBe("btn-primary");

// ✅ Testing user-visible behavior
await expect(page.getByRole("list")).toBeVisible();
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();

Test Isolation

Principle: Each test must run independently with its own state.

Critical Requirements:

  • Every test gets fresh local storage, session storage, data, and cookies
  • Tests cannot depend on execution order or state from other tests
  • Each test should be able to run alone without setup from other tests

Benefits:

  • Improves reproducibility
  • Makes debugging easier
  • Prevents cascading failures
  • Allows parallel execution

Implementation Patterns:

// ✅ Use beforeEach for common setup
test.beforeEach(async ({ page }) => {
  await page.goto("https://example.com/login");
  await page.getByLabel("Username").fill("user");
  await page.getByLabel("Password").fill("pass");
  await page.getByRole("button", { name: "Sign in" }).click();
});

test("first test", async ({ page }) => {
  // Fresh signed-in state
});

test("second test", async ({ page }) => {
  // Independent signed-in state
});

Advanced Pattern - Setup Projects:

  • For expensive operations like authentication, use setup projects
  • Log in once, save state, reuse across tests
  • Dramatically improves test suite performance
  • See Playwright auth documentation for implementation

When Duplication is OK:

  • Simple tests where beforeEach adds complexity
  • When it improves test clarity and readability
  • When it makes tests more self-contained

Avoid Third-Party Dependencies

Principle: Only test code you control.

Rules:

  • Never navigate to external sites you don't control
  • Don't test third-party APIs directly
  • Mock external dependencies instead

Why This Matters:

  • External sites can have cookie banners, overlays, downtime
  • Content can change unexpectedly
  • Slows down test execution
  • Creates flaky tests

Solution - Use Network Mocking:

// ✅ Mock third-party responses
await page.route("**/api/external-service", (route) =>
  route.fulfill({
    status: 200,
    body: JSON.stringify(expectedData),
  }),
);
await page.goto("https://example.com");

ARIA Snapshot Testing

What Are ARIA Snapshots?

Definition: ARIA snapshots provide a YAML representation of the accessibility tree - the semantic structure assistive technologies see.

Why This Is Powerful:

  • Tests what actually matters: semantic structure and accessibility
  • Survives CSS refactoring and styling changes
  • Catches accessibility regressions automatically
  • More resilient than pixel-based visual testing
  • Documents expected page structure in human-readable format
  • Single assertion can validate entire page regions

The Core Pattern

// Assert entire page structure matches
await expect(page.locator("body")).toMatchAriaSnapshot(`
  - banner:
    - heading "Welcome to Our Site" [level=1]
    - navigation:
      - link "Home"
      - link "About"
      - link "Contact"
  - main:
    - region "Featured Content":
      - heading "Latest News" [level=2]
      - list:
        - listitem: link "Article 1"
        - listitem: link "Article 2"
`);

Why ARIA Snapshots Over Individual Assertions

Traditional approach (brittle):

// ❌ Many fragile assertions
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Welcome");
await expect(page.getByRole("link", { name: "Home" })).toBeVisible();
await expect(page.getByRole("link", { name: "About" })).toBeVisible();
await expect(page.getByRole("link", { name: "Contact" })).toBeVisible();
// Breaks if order changes, doesn't validate hierarchy

ARIA snapshot approach (resilient):

// ✅ Single assertion validates structure, hierarchy, and content
await expect(page.locator("nav")).toMatchAriaSnapshot(`
  - navigation:
    - link "Home"
    - link "About"
    - link "Contact"
`);
// Survives styling changes, validates semantic relationships

YAML Format Guide

Basic structure:

- role "accessible name" [attributes]:
    - child role "name"
    - child role "name"

Role: ARIA role (heading, button, link, navigation, list, etc.)

Accessible name: Text users hear from screen readers (in quotes)

Attributes: ARIA attributes in square brackets

  • [level=1] - heading level
  • [checked] or [checked=true] - checkbox state
  • [disabled] - disabled state
  • [expanded=true] - expanded state
  • [pressed=true] - toggle button state

Examples:

// Heading with level
- heading "Dashboard" [level=1]

// Button with state
- button "Menu" [expanded=true]

// Checkbox checked
- checkbox "Accept terms" [checked]

// Disabled input
- textbox "Email" [disabled]

// List structure
- list "Todo Items":
  - listitem: checkbox "Buy groceries"
  - listitem: checkbox "Call mom" [checked]
  - listitem: checkbox "Write tests"

Partial Matching Strategy

Default behavior - subset matching:

// By default, matches if specified children exist (subset match)
await expect(page.locator("ul")).toMatchAriaSnapshot(`
  - list:
    - listitem: text "Feature A"
    - listitem: text "Feature C"
`);
// ✅ Passes even if Feature B exists between them
// ✅ Passes even if more items exist after Feature C

Control child matching with /children property:

The /children property controls how strictly child elements are matched:

1. Subset matching (default):

await expect(page.locator("ul")).toMatchAriaSnapshot(`
  - list:
    /children:
    - listitem: text "Feature A"
    - listitem: text "Feature B"
`);
// ✅ Matches if A and B exist in order
// ✅ Other items can exist before, between, or after

2. Exact matching with deep-equal:

await expect(page.locator("ul")).toMatchAriaSnapshot(`
  - list:
    /children: deep-equal
    - listitem: text "Feature A"
    - listitem: text "Feature B"
    - listitem: text "Feature C"
`);
// ✅ Only passes if exactly these 3 items exist in this exact order
// ❌ Fails if Feature D exists
// ❌ Fails if order changes

3. No children validation:

await expect(page.locator("nav")).toMatchAriaSnapshot(`
  - navigation
`);
// ✅ Validates role exists
// ✅ Doesn't check any children at all

When to use each mode:

Subset (default) - Most flexible:

  • Dynamic lists where count varies
  • Checking specific items exist
  • When order matters but count doesn't
// Check shopping cart has key items, ignore quantity
- region "Cart":
  - listitem: text "Laptop"
  - listitem: text "Mouse"
  // Other items OK, we just care these exist

deep-equal - Most strict:

  • Fixed navigation structures
  • Known static content
  • When exact structure matters
// Main navigation must be exactly this
- navigation:
  /children: deep-equal
  - link "Home"
  - link "Products"
  - link "About"
  - link "Contact"
  // Fails if "Careers" link is added

No children - Structure only:

  • When children are completely dynamic
  • Just validating container exists
// User feed could have any number of posts
- main:
  - region "Feed"
    // Don't validate children at all

Partial matching by omitting attributes:

// Match structure regardless of checkbox state
await expect(page.locator("form")).toMatchAriaSnapshot(`
  - checkbox "Remember me"
  - button "Sign in"
`);
// ✅ Passes whether checkbox is checked or unchecked

Use regex for dynamic content:

await expect(page.locator("header")).toMatchAriaSnapshot(`
  - banner:
    - heading /Welcome, .+/ [level=1]
    - text /Last login: \\d{4}-\\d{2}-\\d{2}/
`);
// Matches any username and date pattern

When to Use ARIA Snapshots

Use ARIA snapshots for:

  • Page layout validation (header, nav, main, footer structure)
  • Form structure testing (labels, inputs, buttons in correct hierarchy)
  • List and table structures
  • Navigation menus
  • Modal dialogs and their contents
  • Component structure validation
  • Accessibility compliance checks
  • Regression detection for structural changes

Use individual locators for:

  • Specific interactions (clicking, typing)
  • Dynamic state changes
  • Conditional logic in tests
  • Precise timing requirements

Combine both:

// Validate structure
await expect(page.locator("dialog")).toMatchAriaSnapshot(`
  - dialog "Confirm Action":
    - heading "Are you sure?" [level=2]
    - text "This action cannot be undone"
    - button "Cancel"
    - button "Confirm"
`);

// Then interact with specific elements
await page.getByRole("button", { name: "Confirm" }).click();

Snapshot Generation Workflow

Generate snapshot on first run:

// Write test with empty string
await expect(page.locator("nav")).toMatchAriaSnapshot(``);

// Run with update flag
// npx playwright test --update-snapshots

// Playwright generates snapshot automatically

Update snapshots after changes:

# Update all snapshots
npx playwright test --update-snapshots

# Short form
npx playwright test -u

Review generated snapshots:

  • Playwright creates patch files showing differences
  • Review patches before committing
  • Ensures structural changes are intentional
  • Documents evolution of page structure

Common Patterns

Page layout validation:

test("homepage structure", async ({ page }) => {
  await page.goto("/");

  await expect(page.locator("body")).toMatchAriaSnapshot(`
    - banner:
      - link "Company Logo"
      - navigation:
        - link "Products"
        - link "About"
        - link "Contact"
    - main:
      - region "Hero":
        - heading "Transform Your Business" [level=1]
        - button "Get Started"
    - contentinfo:
      - text "© 2024 Company Name"
  `);
});

Form structure testing:

test("registration form", async ({ page }) => {
  await page.goto("/register");

  await expect(page.locator("form")).toMatchAriaSnapshot(`
    - textbox "Email"
    - textbox "Password" [type="password"]
    - textbox "Confirm Password" [type="password"]  
    - checkbox "I agree to the terms"
    - button "Create Account"
  `);
});

Table structure validation:

test("data table structure", async ({ page }) => {
  await page.goto("/dashboard");

  await expect(page.locator("table")).toMatchAriaSnapshot(`
    - table:
      - rowgroup:
        - row:
          - columnheader "Name"
          - columnheader "Email"
          - columnheader "Status"
      - rowgroup:
        - row:
          - cell "John Doe"
          - cell "[email protected]"
          - cell "Active"
  `);
});

Modal dialog validation:

test("confirmation dialog", async ({ page }) => {
  await page.goto("/");
  await page.getByRole("button", { name: "Delete" }).click();

  await expect(page.locator("role=dialog")).toMatchAriaSnapshot(`
    - dialog "Confirm Deletion":
      - heading "Delete Item?" [level=2]
      - text "This action cannot be undone"
      - group "Actions":
        - button "Cancel"
        - button "Delete" [pressed=true]
  `);
});

Integration with Accessibility Testing

ARIA snapshots enforce accessibility:

  • Elements must have proper ARIA roles
  • Interactive elements need accessible names
  • Heading hierarchy must be correct
  • Form inputs must be labeled
  • Lists must use proper structure

Example - catches accessibility issues:

<!-- Bad HTML - no accessible name -->
<button>
  <svg><path d="..." /></svg>
</button>
// Snapshot test fails - button has no accessible name
await expect(page.locator("button")).toMatchAriaSnapshot(`
  - button "Delete"  // ❌ Fails - actual button has no name
`);

Forces fix:

<!-- Good HTML - accessible name provided -->
<button aria-label="Delete">
  <svg><path d="..." /></svg>
</button>

Best Practices

Scope snapshots appropriately:

// ✅ Good - specific region
await expect(page.locator('nav')).toMatchAriaSnapshot(...)

// ❌ Too broad - entire page
await expect(page.locator('body')).toMatchAriaSnapshot(...)

// ✅ Better - specific sections
await expect(page.locator('header')).toMatchAriaSnapshot(...)
await expect(page.locator('main')).toMatchAriaSnapshot(...)

Use partial matching for dynamic content:

// Shopping cart with variable item count
await expect(page.locator("aside")).toMatchAriaSnapshot(`
  - region "Cart":
    - heading /Cart \\(\\d+ items?\\)/ [level=2]
    - list:
      - listitem: text /.*/ 
      // Validates structure without checking all items
`);

Combine with web-first assertions:

// Validate structure exists
await expect(page.locator("form")).toMatchAriaSnapshot(`
  - textbox "Email"
  - button "Submit"
`);

// Then validate dynamic states
await expect(page.getByRole("button", { name: "Submit" })).toBeDisabled();

Version control snapshots:

  • Commit snapshot files with code
  • Review snapshot changes in PRs
  • Treat snapshot updates as structural changes
  • Document why snapshots changed

Locator Strategy Framework

The Locator Resilience Hierarchy

Always choose the highest priority locator that works.

This hierarchy is based on production experience with test maintenance:

  1. Role-based locators (HIGHEST PRIORITY)

- Most resilient to UI changes - Aligned with accessibility best practices - Self-documenting

``javascript page.getByRole("button", { name: "Submit" }); page.getByRole("textbox", { name: "Email" }); page.getByRole("heading", { name: "Dashboard" }); ``

  1. Label associations

- Resilient and semantically meaningful - Follows form best practices

``javascript page.getByLabel("Password"); page.getByLabel("Remember me"); ``

  1. Placeholder text

- Good for inputs when labels aren't available - Less stable than labels

``javascript page.getByPlaceholder("Enter email address"); ``

  1. Text content

- Good for unique text - Can break if copy changes

``javascript page.getByText("Welcome back"); ``

  1. Test IDs (when semantic options aren't available)

- Stable but requires code changes - Use data-testid attribute

``javascript page.getByTestId("submit-button"); ``

  1. XPath/CSS selectors (LAST RESORT)

- Most fragile - Only when nothing else works - Document why you need them

Why This Order Matters

Role-based locators win because:

  • They survive CSS refactoring
  • They survive DOM restructuring
  • They survive text changes (use name option)
  • They align with how users actually interact
  • They encourage accessible markup

CSS classes lose because:

// ❌ Breaks when designer changes styles
page.locator(".btn-primary.submit-button.large");

// ✅ Survives style refactoring
page.getByRole("button", { name: "Submit Order" });

Deep DOM paths lose because:

// ❌ Breaks when structure changes
page.locator("div.container > section > form > button:nth-child(3)");

// ✅ Survives restructuring
page.getByRole("form", { name: "Checkout" }).getByRole("button", { name: "Submit" });

Chaining and Filtering Patterns

Narrow scope to reduce brittleness:

// ✅ Chain to narrow context - more resilient
const product = page.getByRole("listitem").filter({ hasText: "Product 2" });

await product.getByRole("button", { name: "Add to cart" }).click();

Filtering Strategies:

// Filter by text content
.filter({ hasText: 'Premium' })

// Filter by another locator (element contains specific child)
.filter({ has: page.getByRole('button', { name: 'Delete' }) })

// Exclude elements
.filter({ hasNotText: 'Sold out' })

// Combine filters
.filter({ hasText: 'Premium' })
.filter({ has: page.getByRole('img') })

Locator Generation Strategy

Never hand-write complex locators - use codegen to discover optimal selectors:

# Generate locator for specific element
npx playwright codegen https://example.com
# Then use Inspector's "Pick Locator" feature

Why generation beats hand-writing:

  • Playwright knows the full context of the page
  • It tests uniqueness automatically
  • It prioritizes according to best practices
  • It handles edge cases (multiple matches, hidden elements)

When to regenerate locators:

  • After UI refactoring
  • When tests become flaky
  • When selectors feel brittle
  • During test review

Web-First Assertions

The Core Pattern: Always Await Expect

Critical Rule: Use web-first assertions that wait and retry.

// ✅ Web-first assertion - waits automatically
await expect(page.getByText("welcome")).toBeVisible();

// ❌ Manual check - no waiting, no retry
expect(await page.getByText("welcome").isVisible()).toBe(true);

Why Manual Assertions Fail

The timing problem:

// ❌ WRONG - checks exactly once
const visible = await page.getByText("welcome").isVisible();
expect(visible).toBe(true);

What happens:

  1. Queries element at that exact millisecond
  2. If not ready, returns false immediately
  3. Test fails even though element appears 100ms later
  4. Creates flaky tests that fail randomly

The web-first solution:

// ✅ RIGHT - polls until timeout
await expect(page.getByText("welcome")).toBeVisible();

What happens:

  1. Queries element repeatedly
  2. Waits up to 5 seconds (default timeout)
  3. Retries if element isn't ready
  4. Only fails if truly never appears
  5. Much more stable

Assertion Strategy Guide

Visibility Assertions:

await expect(locator).toBeVisible(); // Element is visible
await expect(locator).toBeHidden(); // Element is not visible

State Assertions:

await expect(locator).toBeEnabled(); // Interactive element enabled
await expect(locator).toBeDisabled(); // Interactive element disabled
await expect(locator).toBeChecked(); // Checkbox/radio checked
await expect(locator).toBeFocused(); // Element has keyboard focus

Content Assertions:

await expect(locator).toHaveText("exact text"); // Exact match
await expect(locator).toContainText("partial"); // Substring match
await expect(locator).toHaveValue("input value"); // Input value
await expect(locator).toHaveAttribute("href", "/about"); // Attribute value

Collection Assertions:

await expect(locator).toHaveCount(5); // Exact number of elements

Advanced Patterns:

// Multiple conditions
await expect(locator).toBeVisible();
await expect(locator).toHaveClass(/active/);

// Negation
await expect(locator).not.toBeVisible();

// Custom timeout for slow operations
await expect(locator).toBeVisible({ timeout: 10000 });

Soft Assertions Pattern

Use case: Check multiple conditions without stopping on first failure.

// Continue test even if assertions fail
await expect.soft(page.getByTestId("status")).toHaveText("Success");
await expect.soft(page.getByTestId("count")).toHaveText("42");
await expect.soft(page.getByTestId("user")).toHaveText("John");

// Test continues - all failures reported at end
await page.getByRole("link", { name: "next" }).click();

When to use:

  • Checking multiple independent conditions
  • Form validation with many fields
  • Visual regression checks across page
  • Gathering comprehensive failure information

Important: All failures compile and display once test completes.

Debugging Strategy Framework

Debugging Decision Tree

When test fails, follow this sequence:

  1. Check the failure message - What assertion failed?
  2. Verify locator still matches - Did UI change?
  3. Check timing - Did content appear after timeout?
  4. Inspect network - Did API call fail?
  5. Review auto-wait logs - What actionability check failed?

Parallelism Strategy

Default behavior:

  • Tests in different files run in parallel
  • Tests in same file run sequentially
  • Each test gets isolated browser context

Enable file-level parallelism:

test.describe.configure({ mode: "parallel" });

test("runs concurrently 1", async ({ page }) => {});
test("runs concurrently 2", async ({ page }) => {});

When to use:

  • Many independent tests in one file
  • No shared state between tests
  • Tests are truly isolated

Common Patterns and Edge Cases

Dynamic Content Patterns

// ✅ Web-first assertions handle delays automatically
await expect(page.getByText("Loaded")).toBeVisible();

// ❌ Don't add manual waits
await page.waitForTimeout(2000); // brittle, arbitrary

Multiple Elements Pattern

// Get count
const items = page.getByRole("listitem");
const count = await items.count();

// Iterate
for (let i = 0; i < (await items.count()); i++) {
  await items.nth(i).click();
}

// Filter and act on first match
await page.getByRole("listitem").filter({ hasText: "Premium" }).first().click();

Form Interaction Patterns

// Text inputs
await page.getByLabel("Email").fill("[email protected]");

// Clear and fill
await page.getByLabel("Email").clear();
await page.getByLabel("Email").fill("[email protected]");

// Checkboxes
await page.getByLabel("Subscribe").check();
await page.getByLabel("Unsubscribe").uncheck();

// Radio buttons (check the one you want)
await page.getByLabel("Option A").check();

// Select dropdowns
await page.getByLabel("Country").selectOption("USA");
await page.getByLabel("Country").selectOption({ label: "United States" });

// File uploads
await page.getByLabel("Upload").setInputFiles("file.pdf");
await page.getByLabel("Upload").setInputFiles(["file1.pdf", "file2.pdf"]);

Network Request Patterns

// Wait for specific request
const request = page.waitForRequest("**/api/data");
await page.getByRole("button", { name: "Load" }).click();
await request;

// Wait for response
const response = page.waitForResponse("**/api/data");
await page.getByRole("button", { name: "Load" }).click();
await response;

// Mock API response
await page.route("**/api/data", (route) =>
  route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify({ success: true }),
  }),
);

Navigation Patterns

// Wait for navigation to complete
await page.goto("https://example.com", {
  waitUntil: "networkidle", // or 'load', 'domcontentloaded'
});

// Click and wait for navigation
await Promise.all([page.waitForNavigation(), page.getByRole("link", { name: "Next" }).click()]);

// Wait for URL pattern
await page.waitForURL("**/dashboard");

Anti-Patterns to Avoid

Never Use Arbitrary Waits

// ❌ NEVER do this
await page.waitForTimeout(5000);

// ✅ Use web-first assertions
await expect(page.getByText("Loaded")).toBeVisible();

Why:

  • Arbitrary waits make tests slower than necessary
  • Still flaky if content takes longer than wait
  • No indication of what you're waiting for

Don't Chain Promises Manually

// ❌ Manual promise handling
page
  .getByRole("button")
  .click()
  .then(() => {
    return page.getByText("Success").isVisible();
  });

// ✅ Use async/await
await page.getByRole("button").click();
await expect(page.getByText("Success")).toBeVisible();

Don't Re-Query the Same Element

// ❌ Querying multiple times
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByRole("button", { name: "Submit" })).toBeDisabled();

// ✅ Store locator, more efficient
const submitBtn = page.getByRole("button", { name: "Submit" });
await submitBtn.click();
await expect(submitBtn).toBeDisabled();

Don't Test Implementation Details

// ❌ Testing internal state
expect(component._internalState).toBe("loading");
expect(element.className).toContain("loading");

// ✅ Test user-visible behavior
await expect(page.getByRole("status")).toHaveText("Loading...");
await expect(page.getByRole("progressbar")).toBeVisible();

Don't Use Non-Resilient Selectors

// ❌ CSS classes
page.locator(".btn-primary");

// ❌ Deep DOM paths
page.locator("div > section > form > button:nth-child(3)");

// ❌ Generic selectors
page.locator("button");

// ✅ Role-based with name
page.getByRole("button", { name: "Submit Order" });

Integration with playwright-cli

This best practices skill provides the decision-making framework for using playwright-cli effectively. When using playwright-cli:

Apply ARIA snapshot strategy first:

  • Start with ARIA snapshots for broad structural validation
  • Use snapshot command to visualize page structure
  • Validates semantic structure and accessibility in single assertion
  • More resilient than individual locator checks

Apply locator hierarchy:

  • Use snapshot to explore page structure
  • Choose highest-priority locator from the hierarchy
  • Prefer role-based refs over CSS-based refs

Apply assertion strategy:

  • After actions, verify state with appropriate assertions
  • Use web-first assertion patterns in generated code
  • Don't rely on manual timing

Apply debugging framework:

  • Use console and network commands to diagnose
  • Follow debugging decision tree
  • Capture traces for complex failures

Apply test isolation:

  • Use sessions to separate test contexts
  • Don't let state leak between test scenarios
  • Mock external dependencies with route commands

When to Apply This Skill

Trigger Scenarios:

  • Planning test strategy for new features
  • Validating page structure and accessibility
  • Choosing between ARIA snapshots vs individual locators
  • Choosing locators during test creation
  • Reviewing test code for resilience
  • Debugging flaky or failing tests
  • Optimizing test suite performance
  • Advising on test architecture
  • Setting up CI/CD test pipelines
  • Evaluating test quality and maintainability

Key Indicators:

  • Questions about ARIA snapshot usage
  • Discussion of structural validation
  • Questions about locator selection
  • Discussion of test reliability
  • Debugging test failures
  • Test review and refactoring
  • CI/CD configuration for tests
  • Test suite optimization
  • Cross-browser testing strategy
  • Accessibility compliance questions

Output Quality Markers:

  • Uses ARIA snapshots for structural validation
  • Uses role-based locators over CSS selectors
  • Employs web-first assertions consistently
  • Implements proper test isolation
  • Mocks external dependencies
  • Follows locator resilience hierarchy
  • Applies debugging decision tree
  • Tests across multiple browsers
  • Validates accessibility automatically