Vitest config and unit tests
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: { tsconfigPaths: true },
test: {
environment: "jsdom",
include: ["__tests__/**/*.test.{ts,tsx}"],
setupFiles: ["./vitest.setup.ts"],
},
});
import { AsyncLocalStorage } from "node:async_hooks";
(globalThis as unknown as { AsyncLocalStorage: typeof AsyncLocalStorage }).AsyncLocalStorage = AsyncLocalStorage;
import { afterEach, expect, test } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import Home from "@/app/page";
afterEach(cleanup);
test("Home renders a heading and a link", () => {
render(<Home />);
expect(screen.getByRole("heading", { level: 1, name: "Home" })).toBeDefined();
expect(screen.getByRole("link", { name: "About" }).getAttribute("href")).toBe("/about");
});
test("Counter increments on click", () => {
render(<Home />);
const button = screen.getByRole("button");
fireEvent.click(button);
fireEvent.click(button);
expect(button.textContent).toBe("Clicked 2 times");
});
import { afterEach, expect, test, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
vi.mock("@/lib/data", () => ({
getUsers: vi.fn(async () => [{ id: 1, name: "Mocked Ada" }, { id: 2, name: "Mocked Linus" }]),
}));
import UsersPage from "@/app/users/page";
afterEach(cleanup);
test("async Server Component: await it as a function, then render the result", async () => {
const ui = await UsersPage();
render(ui);
expect(screen.getByRole("heading").textContent).toBe("Users (2)");
expect(screen.getAllByRole("listitem").map((li) => li.textContent)).toEqual(["Mocked Ada", "Mocked Linus"]);
});
import { expect, test, vi } from "vitest";
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
import { revalidatePath } from "next/cache";
import { createTodo } from "@/app/actions";
const form = (title: string) => { const f = new FormData(); f.set("title", title); return f; };
test("rejects short titles without revalidating", async () => {
expect(await createTodo({ ok: false }, form("ab"))).toEqual({ ok: false, error: "Title must be at least 3 characters" });
expect(revalidatePath).not.toHaveBeenCalled();
});
test("accepts a valid title and revalidates /todos", async () => {
expect(await createTodo({ ok: false }, form("Buy milk"))).toEqual({ ok: true });
expect(revalidatePath).toHaveBeenCalledWith("/todos");
});
import { expect, test } from "vitest";
import { GET } from "@/app/api/health/route";
test("GET /api/health", async () => {
const res = await GET(new Request("http://localhost/api/health?verbose=1"));
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ status: "ok", uptime: "lab" });
});
import { expect, test } from "vitest";
import { NextRequest } from "next/server";
import * as testing from "next/experimental/testing/server";
import { config, proxy } from "@/proxy";
test("matcher runs proxy only under /dashboard", () => {
expect(testing.unstable_doesMiddlewareMatch({ config, url: "/dashboard/settings" })).toBe(true);
expect(testing.unstable_doesMiddlewareMatch({ config, url: "/about" })).toBe(false);
});
test("the name used in the 16.4.2 proxy docs is not exported by the package", () => {
expect("unstable_doesProxyMatch" in testing).toBe(false);
});
test("redirects to /login without a session cookie", () => {
const res = proxy(new NextRequest("http://localhost/dashboard"));
expect(testing.getRedirectUrl(res)).toBe("http://localhost/login");
});
test("lets the request through with a session cookie", () => {
const res = proxy(new NextRequest("http://localhost/dashboard", { headers: { cookie: "session=abc" } }));
expect(testing.getRedirectUrl(res)).toBeNull();
expect(res.headers.get("x-middleware-next")).toBe("1");
});
Playwright against the production build
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: {
baseURL: "http://localhost:4163",
launchOptions: { executablePath: process.env.CHROMIUM_PATH },
},
webServer: {
command: "npx next start -p 4163",
url: "http://localhost:4163",
reuseExistingServer: !process.env.CI,
},
});
import { expect, test } from "@playwright/test";
test("navigates from Home to About with a client-side transition", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => ((window as unknown as { marker: number }).marker = 42));
await page.getByRole("link", { name: "About" }).click();
await expect(page).toHaveURL("/about");
await expect(page.getByRole("heading", { level: 1 })).toHaveText("About");
expect(await page.evaluate(() => (window as unknown as { marker?: number }).marker)).toBe(42);
});
test("async Server Component renders real data", async ({ page }) => {
await page.goto("/users");
await expect(page.getByRole("heading")).toHaveText("Users (2)");
await expect(page.getByRole("listitem")).toHaveText(["Ada", "Linus"]);
});
test("client component is interactive after hydration", async ({ page }) => {
await page.goto("/");
await page.getByRole("button").click();
await expect(page.getByRole("button")).toHaveText("Clicked 1 times");
});
test("proxy redirects /dashboard without a session", async ({ page }) => {
const res = await page.request.get("/dashboard", { maxRedirects: 0 });
expect(res.status()).toBe(307);
expect(res.headers()["location"]).toBe("/login");
});