frontend-testing
Write tests that start with acceptance criteria, then add implementation tests for robustness. Use when writing unit tests (Vitest), end-to-end tests (Playwright), visual regression tests, or accessibility tests. Emphasizes user-centric testing, semantic locators, accessibility validation, and the balance between acceptance and implementation testing.
What this skill does
# Frontend Testing
Start by writing tests that validate acceptance criteria. Then add implementation tests where they provide value.
## Core Principle
> "The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds
This principle guides testing decisions, but isn't the whole picture:
- **Acceptance criteria tests** verify the system does what users/stakeholders need. These should be stable across refactors.
- **Implementation tests** verify the pieces are robust — edge cases, error handling, complex logic. These may change when you refactor.
Both have value. The anti-pattern to avoid is tests that *only* mirror implementation without validating meaningful behavior.
## When to Load References
Load reference files based on test type:
- **Unit tests with DOM**: `references/locator-strategies.md`
- **E2E tests**: `references/locator-strategies.md`, `references/aria-snapshots.md`
- **Visual regression tests**: `references/visual-regression.md`
- **Accessibility audits**: `references/accessibility-testing.md`
- **Structure validation**: `references/aria-snapshots.md` — consolidate multiple assertions into one
- **All tests**: Start with this file for core workflow
## Workflow
### Step 1: Start with Acceptance Criteria
Before writing any test, identify what the code should do from the user's perspective.
Ask for or extract criteria from:
- Ticket description or user story
- Figma annotations
- Functional requirements
- Product owner clarification
Document criteria as a checklist. These become your first tests.
**Write acceptance tests before reading implementation.** This prevents circular validation where tests just confirm "code does what code does."
### Step 2: Map Criteria to Test Cases
For each criterion, identify:
- **Happy path**: Normal expected behavior
- **Edge cases**: Boundary conditions
- **Error cases**: Invalid inputs, failures
Example mapping:
```
Criterion: "User can filter products by category"
├─ Happy path: Select category, products filter correctly
├─ Edge case: No products match filter, show empty state
├─ Edge case: Clear filter, all products show again
├─ Error case: Filter API fails, show error message
└─ Accessibility: Filter controls are keyboard accessible
```
### Step 3: Add Implementation Tests (Unit Tests)
After acceptance tests pass, add unit tests for implementation robustness:
- **Edge cases** the criteria don't cover (null, undefined, empty arrays, boundary values)
- **Algorithm correctness** for complex calculations
- **Error handling paths** (exceptions, network failures, parse errors)
- **Complex branching logic** hard to exercise through integration tests
- **Performance-sensitive code** that needs specific validation
```
Function: filterProducts(products, category)
├─ Acceptance: Returns matching products (from criteria)
├─ Implementation: Returns empty array when products is null
├─ Implementation: Returns all products when category is empty string
├─ Implementation: Handles case-insensitive category matching
└─ Implementation: Does not mutate original array
```
The distinction: acceptance tests should rarely change on refactor; implementation tests may need updates when internals change.
### Step 4: Choose Test Type
| Scenario | Test Type | Tool |
|----------|-----------|------|
| Pure logic (no DOM) | Unit test | Vitest |
| Component behavior | Unit test with DOM | Vitest + Testing Library |
| User flows, real browser | E2E test | Playwright |
| Semantic structure validation | ARIA snapshot | Playwright `toMatchAriaSnapshot` |
| Visual appearance | VRT | Playwright screenshots |
| Accessibility compliance | A11y test | Playwright + axe-core |
**ARIA snapshots** are particularly valuable for E2E tests. A single snapshot can replace multiple individual assertions while validating the accessibility tree structure.
**DOM Environment for Unit Tests:** Prefer happy-dom over jsdom. It's faster, and its API limitations serve as a useful signal — if happy-dom doesn't support what you're testing, consider whether it belongs in an E2E test instead.
### Step 5: Write Tests Before/Alongside Code
- **Ideal**: Write test first, then implementation (TDD)
- **Acceptable**: Write test immediately after implementing each criterion
- **Avoid**: Write all tests after implementation is "done"
## Test Structure
### Unit Tests (Vitest)
```javascript
describe("calculateDiscount", () => {
describe("when customer has premium membership", () => {
it("applies 20% discount to order total", () => {
// Arrange - Set up test data matching criterion
const order = { total: 100, membership: "premium" };
// Act - Call the function
const result = calculateDiscount(order);
// Assert - Verify expected outcome from requirements
expect(result).toBe(80);
});
});
});
```
### Component Tests (Vitest + Testing Library)
```javascript
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
describe("LoginForm", () => {
describe("when credentials are invalid", () => {
it("displays error message to user", async () => {
const user = userEvent.setup();
render(<LoginForm />);
// Interact using accessible queries
await user.type(
screen.getByLabelText(/email/i),
"[email protected]"
);
await user.type(
screen.getByLabelText(/password/i),
"wrong"
);
await user.click(
screen.getByRole("button", { name: /sign in/i })
);
// Assert on user-visible outcome
expect(
await screen.findByRole("alert")
).toHaveTextContent(/invalid credentials/i);
});
});
});
```
### E2E Tests (Playwright)
```javascript
import { test, expect } from "@playwright/test";
test.describe("Product Catalog", () => {
test.describe("filtering by category", () => {
test("shows only matching products", async ({ page }) => {
await page.goto("/products");
// Use semantic locators
await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
// Assert count, then spot-check first/last
const products = page.getByRole("article");
await expect(products).toHaveCount(5);
await expect(products.first()).toContainText(/electronics/i);
await expect(products.last()).toContainText(/electronics/i);
});
});
});
```
When you need to verify all items, use `Promise.all` for parallel assertions:
```javascript
test("all products match filter", async ({ page }) => {
await page.goto("/products");
await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
const products = await page.getByRole("article").all();
// Parallel assertions — faster than sequential await in a loop
await Promise.all(
products.map(product =>
expect(product.getByText(/electronics/i)).toBeVisible()
)
);
});
```
### E2E Tests with ARIA Snapshots
ARIA snapshots consolidate multiple assertions into one, validating semantic structure:
```javascript
test.describe("Login Page", () => {
test("has correct form structure", async ({ page }) => {
await page.goto("/login");
// One snapshot replaces 5+ individual assertions
await expect(page.getByRole("main")).toMatchAriaSnapshot(`
- heading "Sign In" [level=1]
- textbox "Email"
- textbox "Password"
- button "Sign In"
- link "Forgot password?"
`);
});
test("shows validation errors on empty submit", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: /sign in/i }).click();
await expect(page.getByRole("form")).toMatchAriaSnapshot(`
- textbox "Email"
- text "Email is required"
- textbox "Password"
- text "Password is required"
- button "Sign In"
`);
});
});
```
## Locator PRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.