Skip to content

Instantly share code, notes, and snippets.

@anareyna
Last active March 28, 2025 16:19
Show Gist options
  • Save anareyna/755ffb8ac322dd36816c5f48e1cdde91 to your computer and use it in GitHub Desktop.
Save anareyna/755ffb8ac322dd36816c5f48e1cdde91 to your computer and use it in GitHub Desktop.
Add tests with vitest for nextjs projects...
  1. Add your nextjs project as usual npx create-next-app@latest
  2. Add vitest and other needed dependencies in terminal yarn add -D vitest @testing-library/dom @testing-library/jest-dom @testing-library/user-event jsdom @testing-library/react @vitejs/plugin-react
  3. Add a new task in the package.json file: "test": "vitest"
  4. Modify your tsconfig.json file, add this line inside "compilerOptions": "types": ["node", "@testing-library/jest-dom"]
  5. Add the Demo.tsx, Demo.test.tsx, setupTests.ts and vitest.config.ts files attached below to verify the tests are passing

Automation: If you want a single line to run all these steps use this command in your terminal:

curl -L "https://gist.githubusercontent.com/anareyna/755ffb8ac322dd36816c5f48e1cdde91/raw/f0bd5b4ac6b5ba9f55f43ed864cb2f986f9d0603/setup.sh" -o setup.sh && chmod +x setup.sh && ./setup.sh && rm setup.sh

This will run all the commands from the setup.sh file in this gist

import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import Demo from "./Demo";
describe("App Component", () => {
beforeEach(() => {
const fetchMock = vi.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve({
todos: [
{
userId: 1,
id: 1,
todo: "Sample Todo 1",
completed: false,
},
{
userId: 1,
id: 2,
todo: "Sample Todo 2",
completed: true,
},
{
userId: 1,
id: 3,
todo: "Sample Todo 3",
completed: false,
},
],
}),
})
);
vi.stubGlobal("fetch", fetchMock);
// Mock the IntersectionObserver
const observeMock = vi.fn();
const unobserveMock = vi.fn();
const disconnectMock = vi.fn();
global.IntersectionObserver = vi.fn(function (
this: IntersectionObserver,
callback
) {
(this as any).callback = callback;
return {
observe: observeMock,
unobserve: unobserveMock,
disconnect: disconnectMock,
};
}) as unknown as typeof IntersectionObserver;
});
afterEach(() => {
// Restore the original implementations after each test
vi.unstubAllGlobals();
});
it("renders the initial count and increments the count on button click", () => {
render(<Demo />);
const button = screen.getByRole("button", { name: /count is 0/i });
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(button.textContent).toBe("count is 1");
});
it("fetches and displays the list of todos", async () => {
render(<Demo />);
// Wait for the todos to be rendered
await waitFor(() => {
expect(screen.getByText("Sample Todo 1")).toBeInTheDocument();
expect(screen.getByText("Sample Todo 2")).toBeInTheDocument();
expect(screen.getByText("Sample Todo 3")).toBeInTheDocument();
});
});
it("displays the correct number of todos", async () => {
render(<Demo />);
// Wait for the todos to be rendered
await waitFor(() => {
const todoItems = screen.getAllByRole("listitem");
expect(todoItems.length).toBe(3);
});
});
it("should handle intersection observer correctly", () => {
render(<Demo />);
const observedDiv = screen.getByText("Out of view");
// Simulate the IntersectionObserver callback
act(() => {
const mockObserverInstance = (global.IntersectionObserver as any)
.mock.instances[0];
mockObserverInstance.callback([{ isIntersecting: true }]);
});
expect(observedDiv.textContent).toBe("In view");
});
});
import { useEffect, useRef, useState } from "react";
function Demo() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const [isIntersecting, setIsIntersecting] = useState(false);
const intersectionRef = useRef<HTMLDivElement | null>(null);
type Todo = {
userId: number;
id: number;
todo: string;
completed: boolean;
};
useEffect(() => {
fetch("https://dummyjson.com/todos?limit=5")
.then((res) => res.json())
.then((data) => setTodos(data.todos));
}, []);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
},
{ threshold: 0.5 }
);
if (intersectionRef.current) {
observer.observe(intersectionRef.current);
}
return () => {
if (intersectionRef.current) {
observer.unobserve(intersectionRef.current);
}
};
}, []);
return (
<>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
<ul>
{todos.map((todo: Todo) => (
<li key={todo.id}>{todo.todo}</li>
))}
</ul>
<div
ref={intersectionRef}
style={{
height: "500px",
backgroundColor: isIntersecting ? "green" : "red",
}}
>
{isIntersecting ? "In view" : "Out of view"}
</div>
</>
);
}
export default Demo;
#!/bin/bash
set -e # Exit if any command fails
# Check if jq is installed; install if not
if ! command -v jq &> /dev/null
then
echo "jq not found. Installing jq..."
brew install jq
else
echo "jq is already installed."
fi
echo "Installing dependencies..."
yarn add -D vitest @testing-library/dom @testing-library/jest-dom @testing-library/user-event jsdom @testing-library/react @vitejs/plugin-react
echo "Updating package.json..."
jq '.scripts.test = "vitest"' package.json > package.tmp.json && mv package.tmp.json package.json
echo "Modifying tsconfig.json..."
jq '.compilerOptions.types += ["node", "@testing-library/jest-dom"]' tsconfig.json > tsconfig.tmp.json && mv tsconfig.tmp.json tsconfig.json
echo "Downloading test files..."
curl -L "https://gist.githubusercontent.com/anareyna/755ffb8ac322dd36816c5f48e1cdde91/raw/Demo.tsx" -o Demo.tsx
curl -L "https://gist.githubusercontent.com/anareyna/755ffb8ac322dd36816c5f48e1cdde91/raw/Demo.test.tsx" -o Demo.test.tsx
curl -L "https://gist.githubusercontent.com/anareyna/755ffb8ac322dd36816c5f48e1cdde91/raw/setupTests.ts" -o setupTests.ts
curl -L "https://gist.githubusercontent.com/anareyna/755ffb8ac322dd36816c5f48e1cdde91/raw/vitest.config.ts" -o vitest.config.ts
echo "Setup complete!"
# Remove setup.sh before running tests
rm setup.sh
echo "Running tests..."
yarn test
// Add this file at root or change path in vitest.config.ts file
import * as matchers from "@testing-library/jest-dom/matchers";
import { cleanup } from "@testing-library/react";
import { afterEach, expect } from "vitest";
expect.extend(matchers);
afterEach(() => {
cleanup();
});
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: "./setupTests.ts",
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment