Skip to main content

Problem

When testing components that use useCatMode() (or any context hook):
  • Components throw “hook called outside provider” errors
  • Tests need to wrap component in provider
  • Setting up localStorage before test adds ceremony
  • Easy to forget to test both modes (cat and pro)

Solution: Wrapper Helper + localStorage Setup

Pattern: renderWithContext()

// src/__tests__/helpers/render-with-context.tsx
import { render, RenderOptions } from "@testing-library/react";
import { CatModeProvider } from "@/contexts/cat-mode";
import React from "react";

interface RenderWithContextOptions extends Omit<RenderOptions, "wrapper"> {
  catMode?: boolean;
}

export function renderWithCatMode(
  component: React.ReactElement,
  { catMode = true, ...options }: RenderWithContextOptions = {}
) {
  // Set localStorage before rendering
  localStorage.setItem("appearance-cat-mode", String(catMode));

  const Wrapper = ({ children }: { children: React.ReactNode }) => (
    <CatModeProvider>{children}</CatModeProvider>
  );

  return render(component, { wrapper: Wrapper, ...options });
}

Usage in Tests

// src/__tests__/components/scoring-chips.test.tsx
import { renderWithCatMode } from "../helpers/render-with-context";

describe("ScoringChips", () => {
  beforeEach(() => {
    localStorage.clear();
  });

  test("renders cat mode labels", () => {
    const { getByText } = renderWithCatMode(
      <ScoringChips pawprintz={73} treatz={42} niblz={15} />,
      { catMode: true }
    );

    expect(getByText("Pawprintz")).toBeInTheDocument();
    expect(getByText("Treatz")).toBeInTheDocument();
    expect(getByText("Niblz")).toBeInTheDocument();
  });

  test("renders professional mode labels", () => {
    const { getByText } = renderWithCatMode(
      <ScoringChips pawprintz={73} treatz={42} niblz={15} />,
      { catMode: false }
    );

    expect(getByText("Activity")).toBeInTheDocument();
    expect(getByText("Efficiency")).toBeInTheDocument();
    expect(getByText("Compression")).toBeInTheDocument();
  });

  test("toggles labels on mode change", async () => {
    const { getByRole, getByText } = renderWithCatMode(
      <>
        <button onClick={() => useCatMode().toggle()}>Toggle</button>
        <ScoringChips pawprintz={73} treatz={42} niblz={15} />
      </>,
      { catMode: true }
    );

    // Initially cat mode
    expect(getByText("Pawprintz")).toBeInTheDocument();

    // Click toggle (this is complex; usually test via context separately)
    // For component tests, usually just test at a single mode
  });
});

Testing the Context Itself

// src/__tests__/contexts/cat-mode.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { CatModeProvider, useCatMode } from "@/contexts/cat-mode";

function TestComponent() {
  const { isCatMode, toggle } = useCatMode();
  return (
    <div>
      <p>Mode: {isCatMode ? "cat" : "pro"}</p>
      <button onClick={toggle}>Toggle</button>
    </div>
  );
}

describe("CatModeContext", () => {
  beforeEach(() => {
    localStorage.clear();
  });

  test("reads default value from localStorage (true)", () => {
    render(
      <CatModeProvider>
        <TestComponent />
      </CatModeProvider>
    );

    expect(screen.getByText("Mode: cat")).toBeInTheDocument();
  });

  test("reads stored value from localStorage", () => {
    localStorage.setItem("appearance-cat-mode", "false");

    render(
      <CatModeProvider>
        <TestComponent />
      </CatModeProvider>
    );

    expect(screen.getByText("Mode: pro")).toBeInTheDocument();
  });

  test("toggle updates localStorage", () => {
    render(
      <CatModeProvider>
        <TestComponent />
      </CatModeProvider>
    );

    fireEvent.click(screen.getByRole("button", { name: "Toggle" }));

    expect(localStorage.getItem("appearance-cat-mode")).toBe("false");
  });

  test("toggle changes displayed mode", () => {
    render(
      <CatModeProvider>
        <TestComponent />
      </CatModeProvider>
    );

    expect(screen.getByText("Mode: cat")).toBeInTheDocument();

    fireEvent.click(screen.getByRole("button", { name: "Toggle" }));

    expect(screen.getByText("Mode: pro")).toBeInTheDocument();
  });
});

Snapshot Testing (Optional)

For visual components, capture both modes:
test("matches snapshot in cat mode", () => {
  const { container } = renderWithCatMode(
    <ScoringChips pawprintz={73} treatz={42} niblz={15} />,
    { catMode: true }
  );
  expect(container).toMatchSnapshot();
});

test("matches snapshot in professional mode", () => {
  const { container } = renderWithCatMode(
    <ScoringChips pawprintz={73} treatz={42} niblz={15} />,
    { catMode: false }
  );
  expect(container).toMatchSnapshot();
});

Cleanup Strategy

Always clear localStorage in beforeEach:
beforeEach(() => {
  localStorage.clear();
  jest.clearAllMocks();
});
This prevents test pollution (one test’s localStorage affecting the next).

Extending to Other Contexts

This pattern works for any context-dependent component:
// Generic wrapper for any context
export function renderWithProviders(
  component: React.ReactElement,
  {
    initialState = {},
    ...renderOptions
  }: { initialState?: Record<string, any> } & RenderOptions = {}
) {
  const Wrapper = ({ children }: { children: React.ReactNode }) => (
    <CatModeProvider>
      <ThemeProvider>
        <AnimationsProvider>
          {children}
        </AnimationsProvider>
      </ThemeProvider>
    </CatModeProvider>
  );

  return render(component, { wrapper: Wrapper, ...renderOptions });
}

What to Test

Do test:
  • Context provides correct default value
  • Context reads from localStorage
  • Context updates localStorage on toggle
  • Component renders correct labels in both modes
  • Toggle button changes the mode
Don’t test:
  • React internals (Context implementation is tested by React)
  • localStorage implementation itself
  • Individual label string values (that’s business logic, not testing concern)

SMO1 Test Results

  • 5 tests in cat-mode.test.tsx (context logic)
  • 5 tests in scoring-chips.test.tsx (cat labels)
  • 8 tests in cat-status-badge.test.tsx (pro labels + emojis)
  • 18 total tests, all passing

A2A Prompt Template

When continuing work on SMO1 or similar multi-modal UI projects:
“I’m testing React components that depend on a Context hook (useCatMode()). The context provides isCatMode boolean and persists to localStorage. Create a helper function renderWithCatMode() that:
  1. Sets localStorage appearance-cat-mode before rendering
  2. Wraps component in CatModeProvider
  3. Accepts an option { catMode: boolean } to set the mode
  4. Returns standard RTL render result
Then write tests that verify both cat mode and professional mode labels render correctly.”
This template accelerates future context-dependent component testing across all projects.