All skills
fugazi avatar

/a11y-playwright-testing

@2149484

Accessibility testing for web applications using Playwright (@playwright/test), TypeScript, and axe-core. Use to write, run, or debug WCAG 2.2 AA checks, keyboard and focus tests, ARIA/semantic validation, accessible names, form labels, color contrast, or screen-reader test patterns. Keywords: accessibility, WCAG, axe-core, keyboard navigation, focus management, ARIA.

Use this Skill: https://skilld.dev/gh/fugazi/test-automation-skills-agents/a11y-playwright-testing

This session only. Nothing lands on disk.

referencessnippets-keyboard-focus-semantic.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

A11y Snippets: Keyboard Navigation, Focus Management, Semantic Structure

Part of the a11y-playwright-testing skill. See SKILL.md for full context.

Related: this file was split from the original snippets.md. See the other related files in this folder.

Keyboard Navigation

Tab Order Verification

import { test, expect } from "@playwright/test";

test("tab order is logical", async ({ page }) => {
  await page.goto("/");

  const expectedOrder = [
    page.getByRole("link", { name: /skip to content/i }),
    page.getByRole("link", { name: "Home" }),
    page.getByRole("link", { name: "Products" }),
    page.getByRole("link", { name: "About" }),
    page.getByRole("link", { name: "Contact" }),
  ];

  for (const element of expectedOrder) {
    await page.keyboard.press("Tab");
    await expect(element).toBeFocused();
  }
});

Form Keyboard Navigation

test("form can be completed with keyboard only", async ({ page }) => {
  await page.goto("/login");

  // Tab to email field
  await page.keyboard.press("Tab");
  await expect(page.getByLabel("Email")).toBeFocused();
  await page.keyboard.type("user@example.com");

  // Tab to password
  await page.keyboard.press("Tab");
  await expect(page.getByLabel("Password")).toBeFocused();
  await page.keyboard.type("password123");

  // Tab to remember me checkbox
  await page.keyboard.press("Tab");
  const checkbox = page.getByRole("checkbox", { name: /remember/i });
  await expect(checkbox).toBeFocused();
  await page.keyboard.press("Space"); // Toggle checkbox
  await expect(checkbox).toBeChecked();

  // Tab to submit
  await page.keyboard.press("Tab");
  await expect(page.getByRole("button", { name: "Sign in" })).toBeFocused();

  // Submit with Enter
  await page.keyboard.press("Enter");
  await expect(page).toHaveURL(/dashboard/);
});

Skip Link

test("skip link bypasses navigation", async ({ page }) => {
  await page.goto("/");

  // First Tab focuses skip link
  await page.keyboard.press("Tab");
  const skipLink = page.getByRole("link", { name: /skip to (main|content)/i });
  await expect(skipLink).toBeFocused();
  await expect(skipLink).toBeVisible();

  // Activate skip link
  await page.keyboard.press("Enter");

  // Focus should move to main content
  const main = page.locator('#main, main, [role="main"]').first();
  await expect(main).toBeFocused();
});

Focus Management

Dialog Focus Trap

test("modal dialog traps focus", async ({ page }) => {
  await page.goto("/settings");
  const trigger = page.getByRole("button", { name: "Delete account" });

  // Open modal
  await trigger.click();
  const dialog = page.getByRole("dialog");
  await expect(dialog).toBeVisible();

  // Focus should be on first focusable element inside dialog
  const firstFocusable = dialog.getByRole("button", { name: "Cancel" });
  await expect(firstFocusable).toBeFocused();

  // Tab through dialog elements
  await page.keyboard.press("Tab");
  await expect(
    dialog.getByRole("button", { name: "Confirm delete" }),
  ).toBeFocused();

  // Tab should wrap back to first element (focus trap)
  await page.keyboard.press("Tab");
  await expect(firstFocusable).toBeFocused();

  // Shift+Tab should wrap to last element
  await page.keyboard.press("Shift+Tab");
  await expect(
    dialog.getByRole("button", { name: "Confirm delete" }),
  ).toBeFocused();

  // Escape closes dialog
  await page.keyboard.press("Escape");
  await expect(dialog).toBeHidden();

  // Focus returns to trigger
  await expect(trigger).toBeFocused();
});

Menu Focus Management

test("dropdown menu has proper focus", async ({ page }) => {
  await page.goto("/");
  const menuButton = page.getByRole("button", { name: "Account menu" });

  // Open menu
  await menuButton.click();
  const menu = page.getByRole("menu");
  await expect(menu).toBeVisible();

  // First menu item should be focused or menu itself
  const firstItem = menu.getByRole("menuitem").first();
  await expect(firstItem).toBeFocused();

  // Arrow down moves to next item
  await page.keyboard.press("ArrowDown");
  await expect(menu.getByRole("menuitem").nth(1)).toBeFocused();

  // Arrow up moves to previous item
  await page.keyboard.press("ArrowUp");
  await expect(firstItem).toBeFocused();

  // Escape closes menu and returns focus
  await page.keyboard.press("Escape");
  await expect(menu).toBeHidden();
  await expect(menuButton).toBeFocused();
});

Toast/Alert Focus

test("toast notification announced without stealing focus", async ({
  page,
}) => {
  await page.goto("/settings");

  // Store current focus
  const saveButton = page.getByRole("button", { name: "Save changes" });
  await saveButton.focus();

  // Trigger action that shows toast
  await saveButton.click();

  // Toast appears
  const toast = page.getByRole("status");
  await expect(toast).toBeVisible();
  await expect(toast).toContainText("Settings saved");

  // Focus should NOT move to toast (status messages don't steal focus)
  // Focus stays on or near the action that triggered it
  await expect(saveButton).toBeFocused();
});

Semantic Structure

Landmarks Validation

test("page has required landmarks", async ({ page }) => {
  await page.goto("/");

  // Main landmark
  const main = page.getByRole("main");
  await expect(main).toBeVisible();

  // Navigation landmark
  const nav = page.getByRole("navigation");
  await expect(nav).toBeVisible();

  // Banner (header)
  const banner = page.getByRole("banner");
  await expect(banner).toBeVisible();

  // Content info (footer)
  const footer = page.getByRole("contentinfo");
  await expect(footer).toBeVisible();
});

Heading Hierarchy

test("heading hierarchy is logical", async ({ page }) => {
  await page.goto("/");

  // Get all headings
  const headings = await page.locator("h1, h2, h3, h4, h5, h6").all();

  let previousLevel = 0;
  for (const heading of headings) {
    const tagName = await heading.evaluate((el) => el.tagName);
    const level = parseInt(tagName.charAt(1));

    // Heading level should not skip (e.g., h1 -> h3)
    expect(level - previousLevel).toBeLessThanOrEqual(1);
    previousLevel = level;
  }

  // Page should have exactly one h1
  const h1Count = await page.locator("h1").count();
  expect(h1Count).toBe(1);
});

Form Labels

test("all form inputs have labels", async ({ page }) => {
  await page.goto("/contact");

  const inputs = page.locator(
    'input:not([type="hidden"]):not([type="submit"]), textarea, select',
  );
  const count = await inputs.count();

  for (let i = 0; i < count; i++) {
    const input = inputs.nth(i);

    // Each input should be locatable by role and have an accessible name
    const accessibleName = await input.evaluate((el: HTMLElement) => {
      return (
        el.getAttribute("aria-label") ||
        el.getAttribute("aria-labelledby") ||
        (el as HTMLInputElement).labels?.[0]?.textContent ||
        el.getAttribute("placeholder")
      ); // Placeholder alone is insufficient
    });

    expect(accessibleName, `Input ${i} lacks accessible name`).toBeTruthy();
  }
});

Source: SKILL.md on GitHub

1 warning16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides a comprehensive toolkit for automated accessibility testing using Playwright and axe-core. It includes numerous code snippets, project structure recommendations, and WCAG 2.2 AA compliance checklists. The security analysis identified an indirect prompt injection surface inherent to the process of testing untrusted web content, where external DOM data is processed by the agent.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    4/4 files flagged

Signed by skilld at 2149484. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 4 days ago.

Activeupdated 2 months ago

README badge

README badge for fugazi/test-automation-skills-agents/a11y-playwright-testing