Skip to content

Multi-Layer Testing

Unit tests (Node.js) + browser tests (Chromium) + E2E (Playwright) with Zustand store reset utilities.

What Problem This Solves

You want fast unit tests for logic, real browser tests for component behavior, and E2E tests for critical flows — all in the same project.

Prerequisites

  • vitest with browser mode
  • playwright
  • zustand

Files

store-test-utils.ts

ts
import { type StoreApi } from "zustand";

type ResettableStore<T> = {
  reset: () => void;
  setState: (state: Partial<T>) => void;
  getState: () => T;
};

export function createStoreResettable<T extends object>(store: StoreApi<T>): ResettableStore<T> {
  const initialState = store.getState();

  return {
    reset: () => store.setState(initialState, true),
    setState: (state) => store.setState(state, false),
    getState: () => store.getState(),
  };
}

export function createLocalStorageMock(): Storage {
  const store: Record<string, string> = {};

  return {
    get length() {
      return Object.keys(store).length;
    },
    getItem: (key) => store[key] ?? null,
    setItem: (key, value) => {
      store[key] = value;
    },
    removeItem: (key) => {
      delete store[key];
    },
    clear: () => {
      for (const key of Object.keys(store)) delete store[key];
    },
    key: (index) => Object.keys(store)[index] ?? null,
  };
}

render-with-providers.tsx

tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "@testing-library/react";
import type { ReactNode } from "react";
import { ThemeProvider } from "./theme-provider";

const testQueryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

export function renderWithProviders(ui: ReactNode) {
  return render(
    <QueryClientProvider client={testQueryClient}>
      <ThemeProvider defaultTheme="light">{ui}</ThemeProvider>
    </QueryClientProvider>,
  );
}

unit-setup.ts

ts
import "@testing-library/jest-dom/vitest";

// Mock localStorage for unit tests
const localStorageMock = createLocalStorageMock();
Object.defineProperty(window, "localStorage", {
  value: localStorageMock,
  writable: true,
});

browser-setup.ts

ts
import "@testing-library/jest-dom/vitest";

// Browser tests run in real Chromium, no mocks needed
// But you may want to start MSW here if using API mocking

Example: Store Unit Test

ts
import { describe, it, expect, beforeEach } from "vitest";
import { useAuthStore } from "./auth-store";
import { createStoreResettable } from "./store-test-utils";

const resettableStore = createStoreResettable(useAuthStore);

describe("AuthStore", () => {
  beforeEach(() => {
    resettableStore.reset();
  });

  it("should set session", () => {
    resettableStore.setState({
      token: "abc123",
      isAuthenticated: true,
    });

    expect(resettableStore.getState().isAuthenticated).toBe(true);
  });
});

Example: Component Browser Test

tsx
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { renderWithProviders } from "./render-with-providers";
import { LoginForm } from "./login-form";

describe("LoginForm", () => {
  it("renders email and password inputs", () => {
    renderWithProviders(<LoginForm onSubmit={async () => {}} />);

    expect(screen.getByLabelText("Email")).toBeInTheDocument();
    expect(screen.getByLabelText("Password")).toBeInTheDocument();
  });
});

Example: E2E Test

ts
import { test, expect } from "@playwright/test";

test("user can log in", async ({ page }) => {
  await page.goto("/login");
  await page.fill('[name="email"]', "user@example.com");
  await page.fill('[name="password"]', "password");
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL("/dashboard");
});

Configuration

vitest.config.ts

ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    // Unit tests (Node.js, fast)
    environment: "jsdom",
    setupFiles: ["./tests/unit-setup.ts"],

    // Browser tests (Chromium, real DOM/CSS)
    browser: {
      enabled: true,
      provider: "playwright",
      name: "chromium",
      setupFiles: ["./tests/browser-setup.ts"],
    },
  },
});

Test Strategy

LayerSpeedUse ForEnvironment
Unit< 100msStore logic, pure functions, validationNode.js + jsdom
Browser~1sComponent rendering, interactions, accessibilityChromium
E2E~5sCritical user flows, cross-page navigationPlaywright

Rule of thumb: Write 70% unit tests, 20% browser tests, 10% E2E tests.