Skip to content
cccskills
FreePublished on GitHub

frontend-testing

Vitest + React Testing Library patterns for the Next.js 15 frontend. Co-located component tests, mocking lib/api.ts, testing Server Components, CartProvider harness. Use when writing or reviewing *.test.ts(x) files in frontend/.

The English summary is being prepared. Showing the original description.

Works withSupabase
See how to install

Files included (1)

  • SKILL.md6.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Frontend Testing Skill

Version: 1.0 | Working Directory: frontend/

Write Vitest + React Testing Library tests for $ARGUMENTS following the conventions in this repo.

Stack

  • Vitest — runner and assertion library (vi.mock, expect, vi.fn)
  • @testing-library/react — render + DOM queries
  • @testing-library/jest-dom — matchers (toBeInTheDocument, toHaveTextContent, …)
  • @testing-library/user-event — user interactions
  • jsdom — DOM environment

No Jest. No Enzyme. No snapshot tests unless explicitly requested.

Constitution (Non-Negotiable Rules)

#RuleWhy
1Co-locate tests next to the file under test: Foo.tsx → Foo.test.tsx in the same folderKeeps tests discoverable; matches include: ['**/*.test.{ts,tsx}'] in vitest config
2Mock @/lib/api with vi.mock, not raw fetch (unless testing lib/api.ts itself)Tests the component, not the transport
3Server Components: call them as async functions (await Page({ params: Promise.resolve({slug}) })) then render(jsx)Next 15 + React 19 Server Components aren't rendered by next/navigation stubs
4Use userEvent over fireEvent for anything a user would do (click, type, select)userEvent fires realistic event sequences
5Query by role / label / text, not by test IDs unless there's no accessible alternativeCatches a11y regressions for free
6Cart tests use renderWithCart from @/test/utilsSingle source of truth for CartProvider setup
7Don't test: middleware.ts (edge runtime), Tailwind class presence (brittle), third-party libsLow value, high maintenance
8Strict TypeScript in tests — no any, no @ts-ignore without a comment explaining whySame bar as production code

Quick Start

Pure component (Client)

// frontend/components/AddToCartButton.test.tsx
import { describe, it, expect, vi } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithCart } from "@/test/utils";
import { AddToCartButton } from "./AddToCartButton";

const product = {
  id: "p1", slug: "shirt", name: "Tee", price_cents: 1000,
  currency: "USD", image_url: null, stock: 3, is_active: true,
};

describe("AddToCartButton", () => {
  it("adds the product to the cart on click", async () => {
    const user = userEvent.setup();
    renderWithCart(<AddToCartButton product={product} />);
    await user.click(screen.getByRole("button", { name: /add to cart/i }));
    // assert via a cart-reading sibling or by observing the re-render; don't peek into localStorage
  });

  it("is disabled when out of stock", () => {
    renderWithCart(<AddToCartButton product={{ ...product, stock: 0 }} />);
    expect(screen.getByRole("button", { name: /sold out/i })).toBeDisabled();
  });
});

Server Component page

// frontend/app/(store)/products/[slug]/page.test.tsx
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";

vi.mock("@/lib/api", () => ({
  getProduct: vi.fn(async (slug: string) => ({
    id: "p1", slug, name: "Tee", price_cents: 1000,
    currency: "USD", image_url: null, stock: 3, is_active: true,
  })),
}));

const { default: ProductPage } = await import("./page");

describe("ProductPage", () => {
  it("renders the product name", async () => {
    const jsx = await ProductPage({ params: Promise.resolve({ slug: "shirt" }) });
    render(jsx);
    expect(screen.getByRole("heading", { name: /tee/i })).toBeInTheDocument();
  });
});

lib/api.ts helper test (mock fetch here — this is the boundary)

// frontend/lib/api.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { listProducts, ApiError } from "./api";

beforeEach(() => { vi.restoreAllMocks(); });

describe("listProducts", () => {
  it("returns items on 200", async () => {
    vi.stubGlobal("fetch", vi.fn(async () =>
      new Response(JSON.stringify({ items: [], total: 0 }), { status: 200 })
    ));
    const res = await listProducts();
    expect(res.items).toEqual([]);
  });

  it("throws ApiError on non-2xx", async () => {
    vi.stubGlobal("fetch", vi.fn(async () =>
      new Response(JSON.stringify({ detail: "not_found" }), { status: 404 })
    ));
    await expect(listProducts()).rejects.toBeInstanceOf(ApiError);
  });
});

File Organization

What to testWhere the test file goes
components/Foo.tsxcomponents/Foo.test.tsx (same folder)
app/(store)/products/[slug]/page.tsxapp/(store)/products/[slug]/page.test.tsx
lib/api.tslib/api.test.ts
Shared utilitieslib/<name>.test.ts

What To Cover

Read docs/features/<slug>/plan.md first. Derive tests from the plan's user flows + error mapping:

  • Happy path — one test per documented flow
  • Every error branch in plan.md — map ApiError.detail strings to rendered messages
  • Empty / loading / out-of-stock states — components must render without crashing
  • Gated UI — admin components should handle a missing session gracefully (render nothing or a login prompt)

What NOT To Cover

  • frontend/middleware.ts — runs in the edge runtime; integration-test via real auth flow
  • Tailwind class names — querying .className is brittle
  • CartProvider internals — treat as a black box via useCart()
  • Third-party lib internals (@supabase/ssr, next/image)

Common Patterns

Mocking the Supabase browser client

vi.mock("@/lib/supabase/client", () => ({
  createSupabaseBrowserClient: () => ({
    auth: {
      getSession: vi.fn(async () => ({ data: { session: { access_token: "t" } } })),
      signInWithPassword: vi.fn(async () => ({ data: {}, error: null })),
    },
  }),
}));

Asserting router.refresh() was called

const refresh = vi.fn();
vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh, push: vi.fn() }) }));
// ... trigger the action
expect(refresh).toHaveBeenCalled();

Resetting module-level mocks between tests

import { beforeEach } from "vitest";
beforeEach(() => { vi.clearAllMocks(); });

Running

cd frontend
npm test                  # one-shot (CI mode)
npm run test:watch        # watch mode during dev
npm run test:coverage     # with coverage report

Exemplar Files

  • frontend/test/setup.ts — global test setup (jest-dom + env stubs)
  • frontend/test/utils.tsx — renderWithCart helper
  • frontend/vitest.config.ts — config with jsdom + @ alias

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Supabase Postgres + Storage + Auth operations from FastAPI — service-role client, RLS-aware queries, additive SQL migrations under backend/db/migrations, the create_order RPC, and the product-images bucket. Use when adding tables/columns, writing queries, calling RPCs, or working with Storage.

The English summary is being prepared. Showing the original description.

shanraisshan/learning-x-claude2Updated May 7, 2026

Build FastAPI endpoints in the layered router → service → Supabase pattern, with Pydantic v2 schemas, JWT/admin gating, and consistent HTTPException-based domain errors. Use when adding/modifying any endpoint under backend/.

The English summary is being prepared. Showing the original description.

shanraisshan/learning-x-claude2Updated May 7, 2026

pytest + httpx + TestClient patterns for the FastAPI backend. Dependency overrides for auth, FakeSupabase mocking, HTTPException assertions. Use when writing or reviewing backend/tests/test_*.py.

The English summary is being prepared. Showing the original description.

shanraisshan/learning-x-claude2Updated May 7, 2026

Drive Chrome via the claude-in-chrome MCP to exercise plan.md::Verify steps for a shipped feature. Loads deferred MCP tool schemas, asserts console/network cleanliness, records a GIF for the PR. Used by the browser-verifier agent.

The English summary is being prepared. Showing the original description.

shanraisshan/learning-x-claude2Updated May 7, 2026

Pre-PR code review checklist for this repo. Plan alignment, security gates, layering, dead code, API-surface completeness. Used by the code-reviewer agent.

The English summary is being prepared. Showing the original description.

shanraisshan/learning-x-claude2Updated May 7, 2026

Integrate with the FastAPI backend through the typed wrapper in frontend/lib/api.ts, attach Supabase JWTs for admin calls, and handle errors via ApiError. Use when adding/modifying API calls or wiring auth.

The English summary is being prepared. Showing the original description.

shanraisshan/learning-x-claude2Updated May 7, 2026

See all skills by shanraisshan

Report a problem with this skill