> ## Documentation Index
> Fetch the complete documentation index at: https://docs.soundchecklive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Web Testing

> Testing strategies for the Soundcheck web app

The Soundcheck web app uses Vitest for unit tests and Playwright for end-to-end tests.

## Testing stack

| Tool | Purpose |
| - | - |
| Vitest | Unit tests |
| React Testing Library | Component testing |
| Playwright | E2E tests |

### Vitest configs

| Config | Scope |
| - | - |
| `web/vitest.config.ts` | `web/src/**/*.test.{ts,tsx}` (jsdom) |
| `vitest.config.ts` | root mobile `lib/` and `components/` tests, with the root dependency set (node) |

Setup files: `vitest.setup.env.ts`, `vitest.setup.crypto.ts` and `vitest.setup.ts` (web).

***

## Unit tests

### Running tests

```bash theme={null}
cd web

# Run all web tests
npm test

# Run with coverage
npm run test:coverage

# Run a specific web test file
npx vitest run src/components/ui/Button.test.tsx

# Run a root mobile lib test (root dependency set)
cd .. && npm run test:unit -- lib/navigation/__tests__/deepLinkBackStack.test.ts

# Run a specific test by name
npx vitest run -t "test name"

# Watch mode (web tests)
npx vitest
```

### Test file location

Unit tests are co-located with source files or in `__tests__` directories:

```
components/
├── ui/
│   ├── Button.tsx
│   └── Button.test.tsx
```

Shared `lib/` tests live at the repo root (e.g. `lib/api/httpClient.test.ts`).

### Example unit test

```tsx theme={null}
import { render, screen, fireEvent } from '@testing-library/react';
import { vi } from 'vitest';
import { Button } from './Button';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick}>Click</Button>);

    fireEvent.click(screen.getByText('Click'));
    expect(onClick).toHaveBeenCalled();
  });

  it('is disabled when loading', () => {
    render(<Button loading>Submit</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});
```

***

## E2E tests

### Running E2E tests

```bash theme={null}
cd web

# Run all E2E tests
npm run test:e2e

# Run with UI mode
npm run test:e2e-ui

# Run in debug mode
npm run test:e2e-d

# Run specific test file
npm run test:e2e -- login.spec.ts
```

`npm run test:e2e -- --shard=N/M` does **not** use Playwright's count split.
`web/scripts/run-local-e2e.mjs` rewrites it into a duration-weighted file list
from `automated-tests/shard-durations.json` so long specs do not pile onto one
shard. CI writes `playwright-report/results.json` per shard; refresh the weights
with `node scripts/update-shard-durations.mjs <results.json...>` after adding or
reshaping specs, using the artifacts from a push-to-`dev` (full suite) run. The
script keeps a file's old weight unless every test declared in it ran and passed,
so a PR run's critical-tier subset cannot shrink a heavy file.

Third-party services in e2e run against local emulators: the wrapper starts
Google Maps, Spotify, and context.dev from `tools/emulators` and blanks
analytics and payment scripts (see `tools/emulators/README.md`, "Web app").
`E2E_WEB_EMULATORS=off` uses the real services.

Feature flags in e2e come from `flags/registry.json`: flags whose web surface
sets `"e2e": true` are on, everything else is off (see `flags/README.md`).
Specs read the same state with `isE2eFlagOn` from
`automated-tests/utils/featureFlags.ts`. Tests tagged `@pr-screenshots` only
capture PR images, so the wrapper skips them unless `E2E_PR_SCREENSHOTS=1` or
you name the spec; the nightly suite sets it.

Auth setup signs in through Clerk's testing API on `/dev` (not `/sign-in`,
whose hosted `<SignIn />` breaks ticket redemption). It tries
`clerk.signIn({ emailAddress })` first, then password. The hosted form stays
covered by `auth.spec.ts`. The saved `storageState` JWT lasts about a minute;
specs that run late on a shard should remint with `signInFresh` from
`automated-tests/fixtures/freshAuth.ts` instead of trusting that snapshot.
PR CI sets `NEXT_PUBLIC_FF_CREW_AVAILABILITY_OFFERS=false`, so do not seed
through the venues page — use the venue write helpers in `utils/apiEvents.ts`.
Owner rows have a More options menu (View in org activity) but no Remove
Member — pick a row that also has a role combobox. Activity filter tests
should wait for the request whose query includes the chosen category;
`useAuditLogSWR` does not keep the previous page.

### Test file location

E2E tests are in the `automated-tests/` directory:

```
automated-tests/
├── login.spec.ts
├── gigs.spec.ts
├── setlist.spec.ts
└── fixtures/
    └── test-data.ts
```

### Example E2E test

```ts theme={null}
import { test, expect } from '@playwright/test';

test.describe('Login', () => {
  test('should login with valid credentials', async ({ page }) => {
    await page.goto('/sign-in');

    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('Welcome')).toBeVisible();
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.goto('/sign-in');

    await page.fill('[name="email"]', 'invalid@example.com');
    await page.fill('[name="password"]', 'wrong');
    await page.click('button[type="submit"]');

    await expect(page.getByText('Invalid credentials')).toBeVisible();
  });
});
```

***

## Testing requirements for PRs

### When to write tests

| Change Type | Test Type |
| - | - |
| Utility functions | Unit tests |
| Service methods | Unit tests |
| Custom hooks | Unit tests |
| Component logic | Unit tests |
| User flows | E2E tests |
| New pages | E2E tests |
| Critical paths | E2E tests |

### Test coverage goals

Focus on meaningful coverage:

* Business logic and calculations
* API service methods
* Complex component interactions
* Error handling paths

***

## PR screenshots

For visual changes, capture screenshots with Playwright:

### Capture screenshots

```ts theme={null}
// Example: add a focused spec under automated-tests/ for PR visuals
import { test } from '@playwright/test';

test('capture dashboard screenshots', async ({ page }) => {
  await page.goto('/home');
  await page.screenshot({ path: 'automated-tests/screenshots/home.png', fullPage: true });
});
```

### Run screenshot capture

```bash theme={null}
npm run test:e2e -- path/to/your-screenshot.spec.ts --project=chromium
```

### Add to PR description

```markdown theme={null}
## Screenshots

![Dashboard](https://github.com/soundchecklive/soundcheck-web/blob/BRANCH_NAME/screenshots/dashboard.png?raw=true)
```

***

## Best practices

### Unit tests

1. **Test behavior, not implementation**
   ```tsx theme={null}
   // Good
   expect(screen.getByText('Welcome')).toBeVisible();

   // Avoid
   expect(component.state.isLoggedIn).toBe(true);
   ```

2. **Use data-testid sparingly**
   * Prefer accessible queries: `getByRole`, `getByText`, `getByLabelText`
   * Use `data-testid` only when necessary

3. **Mock at a boundary, not our own code.** Mock the generated API client (through its `generatedApiCore` wrapper), third-party SDKs (Clerk, Stripe, Twilio, Sentry, PostHog, the AI gateway), native Expo and React Native modules, `logger`, `config`, and the clock. Let our own services, hooks and components run for real, so the test fails when they break.
   ```tsx theme={null}
   vi.mock('@/lib/services/generatedApiCore', () => ({
     default: { execute: vi.fn(), api_client: { v1EventsList: vi.fn() } },
   }));
   ```
   * Do not stub our own components and then assert that the stubs rendered. `scripts/check-weak-tests.mjs` (pre-commit and the Conventions job, both advisory) prints a new one as a `MOCK` line.
   * In api-events, test SQL and data rules with a `*.integration.test.ts` against real Postgres rather than a mocked `prisma` or `db/*Crud` module.
   * A test that only checks a mock was called with what the test passed in proves nothing. Keep it only when it pins a request shape the types do not.

4. **Reset shared mocks with `vi.resetAllMocks()`** in `beforeEach`, and set return values inside each test or after the reset. `vi.clearAllMocks()` keeps queued `mockResolvedValueOnce` values, which leak into the next test (pitfalls.mdx #22). Give a `vi.mock` factory its defaults as `vi.fn<(...args: any[]) => any>(async () => value)`, not `vi.fn().mockResolvedValue(value)`. The type argument keeps the loose type of a bare `vi.fn()`, so later `mockResolvedValueOnce` calls still type-check. A reset restores the function passed to `vi.fn` but wipes `mockResolvedValue`, and a lost default can pass by accident. A lost timezone default, for example, falls back to the machine's zone and passes everywhere except UTC CI.

### E2E tests

1. **Use page objects for reusability**
   ```ts theme={null}
   class LoginPage {
     constructor(private page: Page) {}

     async login(email: string, password: string) {
       await this.page.fill('[name="email"]', email);
       await this.page.fill('[name="password"]', password);
       await this.page.click('button[type="submit"]');
     }
   }
   ```

2. **Wait for stable state**
   ```ts theme={null}
   // Wait for loading to complete
   await page.waitForLoadState('networkidle');

   // Wait for specific element
   await page.waitForSelector('[data-testid="content"]');
   ```

3. **Test critical user paths**
   * Login/logout flow
   * Creating new records
   * Editing existing records
   * Error scenarios

***

## Debugging tests

### Vitest

```bash theme={null}
# Run with verbose output
npx vitest run --reporter=verbose

# Run with coverage
npm run test:coverage

# Debug specific test
npx vitest --inspect-brk --no-file-parallelism
```

### Playwright

```bash theme={null}
# Debug mode with browser visible
npm run test:e2e-d

# Generate trace
npm run test:e2e -- --trace on

# View trace
npx playwright show-trace trace.zip
```

***

## CI integration

Tests run automatically in GitHub Actions:

| Trigger | Tests Run |
| - | - |
| Pull Request to `dev` | Unit + critical-tier E2E + every spec file the PR changed |
| Push to `dev` | Unit + full E2E suite (deploy gate) |
| Nightly | Full E2E suite with the coverage gate in strict mode |

The **Smoke** tier (`"tier": "critical"` in `automated-tests/coverage-matrix.json`) is PR-blocking: A1 sign-in, H1 home, G1 create gig, G2 gigs list → detail, GD1 gig header edit, E1 member accepts offer. **Regression** (`tier: regression`) and **nice-to-have** (`tier: nice-to-have`) rows run on push-to-`dev` and nightly only. Cut workflows were removed from the repo per the PostHog keep/cut matrix; see `web/automated-tests/README.md`.

`web/scripts/e2e-critical-tier.mjs` turns critical rows plus changed spec files into the Playwright `--grep`. Locally, `npm run test:e2e -- --critical --changed-since=origin/dev` matches PR CI. Title tags `@smoke`, `@regression`, and `@nice-to-have` document the bucket; CI reads matrix tiers, not tags alone.

CI runs `npm run test:coverage` in `web/` for unit tests.

### Viewing results

* Check the Actions tab in GitHub
* Failed tests include screenshots and traces
* Coverage reports are generated for PRs


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.