# Test Frontend

> Write, review, and audit frontend tests for a React + Vitest + Playwright codebase. Applies ISTQB CTFL v4.0 standards (black-box techniques, test pyramid, risk-based prioritization, defect taxonomy) combined with E2E-first philosophy and AAA structure. Triggers on "write tests", "test this component", "test coverage", "Playwright", "Vitest", "RTL", "E2E for", or any request to write or review React/frontend tests.

- Skill: `talgacapri/test-frontend` (Agent Skill)
- Install (CLI): `npx skillmds@latest add talgacapri/test-frontend`
- Raw SKILL.md: https://api.skillmd.com/api/skills/talgacapri/test-frontend/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: talgacapri (https://skillmd.com/u/talgacapri)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/talgacapri/test-frontend

---


# Frontend Testing Skill

Stack: **React · Vitest · React Testing Library · Playwright**
Standard: **ISTQB CTFL v4.0.1** (ISO/IEC/IEEE 29119 aligned)

---

## ISTQB Foundation: The Seven Testing Principles

Apply these every time. They are not optional ceremony — they shape every decision below.

1. **Testing shows presence, not absence of defects.** Tests reduce risk; they don't prove correctness.
2. **Exhaustive testing is impossible.** Use risk-based prioritization (see §Risk) to focus effort.
3. **Early testing saves time and money.** Write tests before or alongside code, not after.
4. **Defects cluster together.** When one bug appears in a component, look for siblings. Retest the cluster.
5. **Tests wear out.** Rotate and extend tests each sprint. The same suite repeated blindly misses new defects.
6. **Testing is context dependent.** Alpha-stage product? Risk tolerance is different from a financial product in production.
7. **Absence-of-defects fallacy.** Passing tests don't mean users are happy. Validate behavior, not just spec conformance.

---

## Test Levels (ISTQB) Mapped to This Stack

| ISTQB Level | Our Implementation | Tooling |
|---|---|---|
| Component (Unit) | Pure functions, utilities, custom hooks | Vitest |
| Component Integration | Components with children, context, stores | Vitest + RTL |
| System | Full user flows end-to-end | Playwright |
| Acceptance | UAT scenarios / acceptance criteria from user stories | Playwright (ATDD) |

---

## The Test Pyramid: Where to Spend Effort

```
         /  E2E (Playwright)  \       ← few, slow, high confidence
        /  Integration Tests   \      ← moderate, medium speed
       /  Unit Tests (Vitest)   \     ← many, fast, isolated
      /__________________________\
```

**Rules:**
- Unit tests: pure functions and custom hooks ONLY. No React components.
- Integration tests: components with real children, real context, minimal mocking (≤2 mocks per test).
- E2E tests: full user flows. If you need 3+ mocks to write a unit or integration test, write E2E instead.

---

## Test Types (ISTQB §2.2.2)

| Type | Frontend Application |
|---|---|
| **Functional** | Does the component do what the user story says? |
| **Non-functional** | Accessibility (axe-core), performance (Core Web Vitals), usability |
| **Black-box** | Test via user-visible behavior — roles, labels, text — never internal state |
| **White-box** | Branch coverage in business logic utilities (Vitest coverage report) |
| **Regression** | Every bug fix gets a new test. Run full suite in CI on every PR. |
| **Confirmation** | After a fix: re-run the failing test and verify it passes. |

---

## Testing Techniques (ISTQB §4)

### Black-Box Techniques (apply to all component tests)

**Equivalence Partitioning (EP)**
Group inputs that the component treats identically. Test one value per partition.
```tsx
// Button disabled prop — three partitions: true, false, undefined (defaults to false)
it('should be clickable when enabled', ...)      // valid partition
it('should be inert when disabled', ...)          // invalid partition
it('should default to enabled when prop absent', ...) // default partition
```

**Boundary Value Analysis (BVA)**
Apply to numeric inputs, string lengths, list sizes. Use 3-value BVA: boundary - 1, boundary, boundary + 1.
```tsx
// Pagination showing max 10 items
it('renders 9 items', ...)   // below boundary
it('renders 10 items', ...)  // at boundary
it('renders 11 items — truncates or paginates', ...) // above boundary
```

**Decision Table Testing**
Use when a component has multiple conditions producing different outcomes (e.g. form validation).
```
| isLoggedIn | hasPermission | isExpired | Result        |
|------------|---------------|-----------|---------------|
| true       | true          | false     | Show content  |
| true       | true          | true      | Show refresh  |
| true       | false         | -         | Show 403      |
| false      | -             | -         | Redirect login|
```
Write one test per column.

**State Transition Testing**
For components with finite states (modals, stepper flows, auth states):
```tsx
// All States coverage minimum: test every state is reachable
// Valid Transitions coverage preferred: test every allowed transition
// e.g. idle → loading → success / error → idle
```

### White-Box Techniques (utilities and hooks)

**Statement Coverage:** Every line of a utility executes under test. Target: 100%.
**Branch Coverage:** Every conditional branch (if/else, ternary, switch) is exercised. Target: ≥95%.
Run `pnpm test:coverage` to verify. Branch coverage subsumes statement coverage.

### Experience-Based Techniques

**Error Guessing:** Before writing a test, list: what would a developer likely get wrong here? Null inputs, empty arrays, missing locale, timezone edge cases, race conditions on async.

**Exploratory Testing:** After writing scripted tests, spend 10 minutes clicking through the component freely. Log anomalies as new test cases.

**Checklist-Based Testing:** For each PR touching UI, run through:
- [ ] Renders without crash
- [ ] Keyboard navigable
- [ ] Screen reader roles correct
- [ ] Loading state shown
- [ ] Error state shown
- [ ] Empty state shown
- [ ] Responsive at 375px and 1280px

---

## Core Philosophy

**E2E tests are the default for user-facing behavior.**
Unit tests are for pure functions only. Component unit tests are an anti-pattern — they test the wrong thing (implementation) and break on every refactor.

**Test behavior, not implementation.**
Never `instance.setState()`, never `component.props`, never internal variable names.

**Minimal mocking.**
Real code > fake code. If you need to mock more than 2 things, that's a signal to write E2E instead.

**Selector priority** (from strongest to weakest semantic signal):
`getByRole` > `getByLabelText` > `getByPlaceholderText` > `getByText` > `getByTestId` > CSS selectors (never).

---

## File Structure

```
src/
  components/
    MyComponent/
      index.tsx
      index.test.tsx       ← co-located unit/integration test (Vitest)
  utils/
    format.ts
    format.test.ts         ← unit test for pure function

e2e/
  tests/
    checkout.spec.ts       ← E2E user flow (Playwright)
    auth.spec.ts
  utils/
    test-helpers.ts        ← shared E2E setup helpers
```

---

## Test Structure: AAA Pattern (ISTQB)

Every test follows Arrange → Act → Assert. No exceptions.

```tsx
it('should disable submit when form is invalid', () => {
  // Arrange
  render(<ContactForm />)
  const submitButton = screen.getByRole('button', { name: /submit/i })

  // Act
  await userEvent.type(screen.getByLabelText(/email/i), 'not-an-email')
  await userEvent.tab() // trigger blur/validation

  // Assert
  expect(submitButton).toBeDisabled()
})
```

---

## Test Naming

Format: `should [expected outcome] when [condition]`

```tsx
it('should show error message when email format is invalid')
it('should call onSubmit when form fields are valid')
it('should disable input when isReadOnly is true')
it('should render empty state when no items are provided')
```

---

## Required Test Scenarios (Minimum)

Every component test file must include:

| Scenario | Always Required |
|---|---|
| Renders without crashing | ✅ |
| Required props render correctly | ✅ |
| Optional props with defaults | ✅ |
| null / undefined / empty inputs | ✅ |
| Boundary values (if numeric or list input) | ✅ |

Conditional (include when the feature is present):

| Feature | Add these tests |
|---|---|
| `onClick` / `onSubmit` | Triggers once, passes correct args |
| Async / API call | Loading state, success state, error state |
| `useEffect` | Runs on mount, runs on dep change, cleanup |
| Routing | Navigation happens, params passed |
| Form | Validates, submits, shows field errors |
| Pagination | At boundary, below, above |

---

## Incremental Workflow

When testing a directory or feature, never generate all tests at once.

```
For each file (ordered: utilities → hooks → simple components → complex → integration):
  1. Write test
  2. Run: pnpm test path/to/file.test.tsx
  3. PASS → mark done, proceed to next
  4. FAIL → fix first, do not continue until green
```

Complexity > 50 lines of logic? Extract to a hook or utility, then test that in isolation first.

---

## Coverage Targets

| Metric | Target |
|---|---|
| Statement coverage | 100% |
| Branch coverage | ≥95% |
| Function coverage | 100% |
| Line coverage | ≥95% |

Run: `pnpm test:coverage` and review the HTML report in `coverage/`. Fix uncovered branches before marking a story done.

---

## E2E Test Structure (Playwright)

```typescript
// e2e/tests/checkout.spec.ts
import { test, expect } from '@playwright/test'
import { createTestAccount, addFunds } from '../utils/test-helpers'

test.describe('Checkout', () => {
  test.beforeEach(async ({ page, context }) => {
    await createTestAccount(page, { status: 'active' })
    const cookies = await context.cookies()
    const accountId = cookies.find(c => c.name === 'account_id')?.value
    await addFunds({ accountId, amount: 10000 })
  })

  test('user can complete checkout with default payment method', async ({ page }) => {
    // Arrange
    await page.goto('/catalog')

    // Act
    await page.getByRole('heading', { name: 'Product Name' }).click()
    await page.getByRole('button', { name: /buy now/i }).click()
    await page.getByRole('button', { name: /confirm/i }).click()

    // Assert
    await expect(page.getByRole('heading', { name: /order confirmed/i })).toBeVisible()
  })
})
```

**E2E Selector Rules:**
1. `getByRole` — always first choice
2. `getByLabel` — for form inputs
3. `getByText` — for static content
4. `getByTestId` — last resort when no accessible selector exists
5. CSS selectors — never

---

## Risk-Based Test Prioritization (ISTQB §5.1.5 + §5.2)

Before writing tests, classify each area by:
- **Risk likelihood** (how often does this area change?)
- **Risk impact** (what fails if this is broken? payments > UI polish)

Execute in this order:
1. High likelihood + High impact → full coverage, E2E + integration
2. Low likelihood + High impact → integration + boundary tests
3. High likelihood + Low impact → smoke tests
4. Low likelihood + Low impact → minimal, if time allows

Example for fintech: payment flows, auth, KYC steps = highest risk. Animations, empty states = lowest risk. Tune this for your domain.

---

## Defect Reporting (ISTQB §5.5)

When a test fails and you're logging a bug, include:

```
Title: [Component] [Behavior] when [condition]
Severity: Critical / Major / Minor / Cosmetic
Steps to Reproduce:
  1. Navigate to /page
  2. Click X
  3. Enter Y
Expected: Z
Actual: W
Test that catches it: [link to spec file + test name]
Root Cause (if known): Error / Defect / Environmental
```

The distinction matters (ISTQB §1.2.3):
- **Error** = developer mistake (e.g., wrong logic)
- **Defect** = the bug in the code (e.g., off-by-one)
- **Failure** = what the user sees (e.g., wrong total shown)

---

## Static Testing Checklist (ISTQB §3) — PR Review

Before merging any frontend PR, confirm:

- [ ] No unused variables or imports (ESLint clean)
- [ ] No hardcoded strings that should be i18n keys
- [ ] Accessibility attributes present (aria-label, role)
- [ ] No `console.log` left in production code
- [ ] Component props typed (no `any`)
- [ ] Error boundaries in place for async data sections

---

## Key Commands

```bash
# Run all unit/integration tests
pnpm test

# Watch mode during development
pnpm test:watch

# Run a specific test file
pnpm test src/components/Button/index.test.tsx

# Coverage report
pnpm test:coverage

# Run E2E tests
pnpm playwright test

# Run specific E2E spec
pnpm playwright test e2e/tests/checkout.spec.ts

# Run E2E in headed mode (debugging)
pnpm playwright test --headed
```

---

## Collaboration-Based Testing (ISTQB §4.5)

When working with designers and engineers on user stories:

**ATDD approach:** Before any code is written, define acceptance criteria in Given/When/Then format. These become your Playwright tests.

```gherkin
Given a logged-in user with sufficient balance
When they tap "Send Money" and complete the flow
Then the transaction appears in history with correct amount and status
```

**INVEST check for stories before testing:**
- Independent: test can run without other test state
- Negotiable: acceptance criteria written collaboratively
- Valuable: tests a real user need, not an implementation detail
- Estimable: you can estimate testing effort
- Small: one user story = one focused test scenario
- Testable: acceptance criteria exist and are unambiguous

