mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
chore: add playwright cursor skill
This commit is contained in:
parent
25aad38ca4
commit
d52225c18d
57 changed files with 25244 additions and 0 deletions
|
|
@ -0,0 +1,359 @@
|
|||
# Accessibility Testing
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Axe-Core Integration](#axe-core-integration)
|
||||
2. [Keyboard Navigation](#keyboard-navigation)
|
||||
3. [ARIA Validation](#aria-validation)
|
||||
4. [Focus Management](#focus-management)
|
||||
5. [Color & Contrast](#color--contrast)
|
||||
|
||||
## Axe-Core Integration
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
npm install -D @axe-core/playwright
|
||||
```
|
||||
|
||||
### Basic A11y Test
|
||||
|
||||
```typescript
|
||||
import { test, expect } from "@playwright/test";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
|
||||
test("homepage should have no a11y violations", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const results = await new AxeBuilder({ page }).analyze();
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
### Scoped Analysis
|
||||
|
||||
```typescript
|
||||
test("form accessibility", async ({ page }) => {
|
||||
await page.goto("/contact");
|
||||
|
||||
// Analyze only the form
|
||||
const results = await new AxeBuilder({ page })
|
||||
.include("#contact-form")
|
||||
.analyze();
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
|
||||
test("ignore known issues", async ({ page }) => {
|
||||
await page.goto("/legacy-page");
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.exclude(".legacy-widget") // Skip legacy component
|
||||
.disableRules(["color-contrast"]) // Disable specific rule
|
||||
.analyze();
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
### A11y Fixture
|
||||
|
||||
```typescript
|
||||
// fixtures/a11y.fixture.ts
|
||||
import { test as base } from "@playwright/test";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
|
||||
type A11yFixtures = {
|
||||
makeAxeBuilder: () => AxeBuilder;
|
||||
};
|
||||
|
||||
export const test = base.extend<A11yFixtures>({
|
||||
makeAxeBuilder: async ({ page }, use) => {
|
||||
await use(() =>
|
||||
new AxeBuilder({ page }).withTags([
|
||||
"wcag2a",
|
||||
"wcag2aa",
|
||||
"wcag21a",
|
||||
"wcag21aa",
|
||||
]),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Usage
|
||||
test("dashboard a11y", async ({ page, makeAxeBuilder }) => {
|
||||
await page.goto("/dashboard");
|
||||
const results = await makeAxeBuilder().analyze();
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
### Detailed Violation Reporting
|
||||
|
||||
```typescript
|
||||
test("report a11y issues", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const results = await new AxeBuilder({ page }).analyze();
|
||||
|
||||
// Custom failure message with details
|
||||
const violations = results.violations.map((v) => ({
|
||||
id: v.id,
|
||||
impact: v.impact,
|
||||
description: v.description,
|
||||
nodes: v.nodes.map((n) => n.html),
|
||||
}));
|
||||
|
||||
expect(violations, JSON.stringify(violations, null, 2)).toHaveLength(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Keyboard Navigation
|
||||
|
||||
### Tab Order Testing
|
||||
|
||||
```typescript
|
||||
test("correct tab order in form", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
|
||||
// Start from the beginning
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.getByLabel("Email")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.getByLabel("Password")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.getByRole("button", { name: "Sign up" })).toBeFocused();
|
||||
});
|
||||
```
|
||||
|
||||
### Keyboard-Only Interaction
|
||||
|
||||
```typescript
|
||||
test("complete flow with keyboard only", async ({ page }) => {
|
||||
await page.goto("/products");
|
||||
|
||||
// Navigate to product with keyboard
|
||||
await page.keyboard.press("Tab"); // Skip to main content
|
||||
await page.keyboard.press("Tab"); // First product
|
||||
await page.keyboard.press("Enter"); // Open product
|
||||
|
||||
await expect(page).toHaveURL(/\/products\/\d+/);
|
||||
|
||||
// Add to cart with keyboard
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Tab"); // Navigate to "Add to Cart"
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
await expect(page.getByRole("alert")).toContainText("Added to cart");
|
||||
});
|
||||
```
|
||||
|
||||
### Skip Links
|
||||
|
||||
```typescript
|
||||
test("skip link works", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.keyboard.press("Tab");
|
||||
const skipLink = page.getByRole("link", { name: /skip to main/i });
|
||||
await expect(skipLink).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
// Focus should move to main content
|
||||
await expect(page.getByRole("main")).toBeFocused();
|
||||
});
|
||||
```
|
||||
|
||||
### Escape Key Handling
|
||||
|
||||
```typescript
|
||||
test("escape closes modal", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
await page.getByRole("button", { name: "Settings" }).click();
|
||||
|
||||
const modal = page.getByRole("dialog");
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await expect(modal).toBeHidden();
|
||||
// Focus should return to trigger
|
||||
await expect(page.getByRole("button", { name: "Settings" })).toBeFocused();
|
||||
});
|
||||
```
|
||||
|
||||
## ARIA Validation
|
||||
|
||||
### Role Verification
|
||||
|
||||
```typescript
|
||||
test("correct ARIA roles", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
// Verify landmark roles
|
||||
await expect(page.getByRole("navigation")).toBeVisible();
|
||||
await expect(page.getByRole("main")).toBeVisible();
|
||||
await expect(page.getByRole("contentinfo")).toBeVisible(); // footer
|
||||
|
||||
// Verify interactive roles
|
||||
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
||||
await expect(page.getByRole("search")).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### ARIA States
|
||||
|
||||
```typescript
|
||||
test("aria-expanded updates correctly", async ({ page }) => {
|
||||
await page.goto("/faq");
|
||||
|
||||
const accordion = page.getByRole("button", { name: "Shipping" });
|
||||
|
||||
// Initially collapsed
|
||||
await expect(accordion).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
await accordion.click();
|
||||
|
||||
// Now expanded
|
||||
await expect(accordion).toHaveAttribute("aria-expanded", "true");
|
||||
|
||||
// Content is visible
|
||||
const panel = page.getByRole("region", { name: "Shipping" });
|
||||
await expect(panel).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Live Regions
|
||||
|
||||
```typescript
|
||||
test("live region announces updates", async ({ page }) => {
|
||||
await page.goto("/checkout");
|
||||
|
||||
// Find live region
|
||||
const liveRegion = page.locator('[aria-live="polite"]');
|
||||
|
||||
await page.getByLabel("Quantity").fill("3");
|
||||
|
||||
// Live region should update with new total
|
||||
await expect(liveRegion).toContainText("Total: $29.97");
|
||||
});
|
||||
```
|
||||
|
||||
## Focus Management
|
||||
|
||||
### Focus Trap in Modal
|
||||
|
||||
```typescript
|
||||
test("focus trapped in modal", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "Open Modal" }).click();
|
||||
|
||||
const modal = page.getByRole("dialog");
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Get all focusable elements in modal
|
||||
const focusableElements = modal.locator(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
const count = await focusableElements.count();
|
||||
|
||||
// Tab through all elements, should stay in modal
|
||||
for (let i = 0; i < count + 1; i++) {
|
||||
await page.keyboard.press("Tab");
|
||||
const focused = page.locator(":focus");
|
||||
await expect(modal).toContainText((await focused.textContent()) || "");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Focus Restoration
|
||||
|
||||
```typescript
|
||||
test("focus returns after modal close", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const trigger = page.getByRole("button", { name: "Delete Item" });
|
||||
await trigger.click();
|
||||
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
// Focus should return to the trigger
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
```
|
||||
|
||||
## Color & Contrast
|
||||
|
||||
### High Contrast Mode
|
||||
|
||||
```typescript
|
||||
test("works in high contrast mode", async ({ page }) => {
|
||||
await page.emulateMedia({ forcedColors: "active" });
|
||||
await page.goto("/");
|
||||
|
||||
// Verify key elements are visible
|
||||
await expect(page.getByRole("navigation")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible();
|
||||
|
||||
// Take screenshot for visual verification
|
||||
await expect(page).toHaveScreenshot("high-contrast.png");
|
||||
});
|
||||
```
|
||||
|
||||
### Reduced Motion
|
||||
|
||||
```typescript
|
||||
test("respects reduced motion preference", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto("/");
|
||||
|
||||
// Animations should be disabled
|
||||
const hero = page.getByTestId("hero-animation");
|
||||
const animation = await hero.evaluate(
|
||||
(el) => getComputedStyle(el).animationDuration,
|
||||
);
|
||||
|
||||
expect(animation).toBe("0s");
|
||||
});
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
### A11y as CI Gate
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: "a11y",
|
||||
testMatch: /.*\.a11y\.spec\.ts/,
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .github/workflows/a11y.yml
|
||||
- name: Run accessibility tests
|
||||
run: npx playwright test --project=a11y
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Problem | Solution |
|
||||
| ----------------------------- | ---------------------------- | ------------------------------------------ |
|
||||
| Testing a11y only on homepage | Misses issues on other pages | Test all critical user flows |
|
||||
| Ignoring all violations | No value from tests | Address or explicitly exclude known issues |
|
||||
| Only automated testing | Misses many a11y issues | Combine with manual testing |
|
||||
| Testing without screen reader | Misses interaction issues | Test with VoiceOver/NVDA periodically |
|
||||
|
||||
## Related References
|
||||
|
||||
- **Locators**: See [locators.md](../core/locators.md) for role-based selectors
|
||||
- **Visual testing**: See [test-suite-structure.md](../core/test-suite-structure.md) for screenshot comparison
|
||||
Loading…
Add table
Add a link
Reference in a new issue