# Component Testing

Custom frontend components are covered by co-located unit tests.
Write the test first, then implement until it passes.

This is the unit layer.
End-to-end flows are covered separately ([e2e-testing.md](./e2e-testing.md)).

## What to test

| Target | Test? |
| --- | --- |
| Components you write under `frontend/src/components/` | Yes |
| Standard UI-library components | No — tested upstream |
| Vendored primitives in `src/components/ui/` | No — tested upstream |
| Pages (`src/pages/**/page.tsx`) | No — covered by e2e |
| Presentational wrapper with no logic | Optional (opt out below) |

Page-local components under `src/pages/**/components/` (tables, forms, detail
panels) should be tested the same way, though it is not yet enforced.

## Enforcement

`erp-kit verify` (run from the repo root, so it discovers apps under `apps/*`)
fails when a component under `src/components/` has no co-located `<name>.test.tsx`
(excluding `ui/` and `*.test.tsx`).

Opt a file out with a marker comment:

```tsx
/* no-test: static layout, no logic */
```

The check verifies the test exists, not the order it was written.

## Tooling

Provided by the scaffold:

- **Runner** — vitest, `jsdom` environment (`pnpm test`, `pnpm test:watch`)
- **Render & assert** — Testing Library + jest-dom
- **Interaction** — user-event

## Steps

1. List the behaviors. Each prop variation, conditional render, and interaction
   is one case.
2. Write the failing `<name>.test.tsx`, next to the component.
3. Implement until green.
4. Run `pnpm test` and `pnpm typecheck` (test files are typechecked too).

## Patterns

### Query by role and text

Prefer accessible queries: `getByRole`, then `getByLabelText`, then `getByText`.
Assert what the user sees, not internal markup.

```tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ErrorFallback } from "./error-fallback";

describe("error-fallback", () => {
  it("shows the message", () => {
    render(<ErrorFallback message="Failed to load." />);
    expect(screen.getByText("Failed to load.")).toBeInTheDocument();
  });

  it("calls onReset when the retry button is clicked", async () => {
    const onReset = vi.fn();
    render(<ErrorFallback onReset={onReset} />);
    await userEvent.click(screen.getByRole("button", { name: /try again/i }));
    expect(onReset).toHaveBeenCalledOnce();
  });
});
```

Render real child components and assert on their output. Do not mock them.

### Mock external dependencies

Mock navigation, auth, and data fetching so the test covers only this
component's logic. Use `vi.hoisted` so the handles exist before the mocks run.

```tsx
const { logout, navigate, useQueryMock } = vi.hoisted(() => ({
  logout: vi.fn(),
  navigate: vi.fn(),
  useQueryMock: vi.fn(),
}));

vi.mock("@tailor-platform/app-shell", () => ({
  useAuth: () => ({ logout }),
  useNavigate: () => navigate,
}));
vi.mock("urql", () => ({ useQuery: () => useQueryMock() }));

import { UserProfileMenu } from "./user-profile-menu";
// useQueryMock.mockReturnValue([{ data: { ... } }]) per test
```

### Forms

Test the visible behavior: validation errors on empty submit, and a successful
submit calling the mutation with the entered values.
Do not re-test the form library or schema validator.
