con un clic
unit-tests
// Jest + React Testing Library best practices for Wonder Blocks unit tests. Use when creating or editing `.test.ts` / `.test.tsx` files.
// Jest + React Testing Library best practices for Wonder Blocks unit tests. Use when creating or editing `.test.ts` / `.test.tsx` files.
Storybook best practices for Wonder Blocks component stories. Use when creating or editing `.stories.tsx` / `.stories.ts` files.
Implements user interfaces using the Wonder Blocks (WB) design system — Khan Academy's React component library. Use this skill whenever the user asks you to build, modify, or review UI components; wants to use or map WB tokens for colors/spacing/typography (including translating Figma designs to WB components and tokens); or asks how to do something "the Wonder Blocks way". If the user is building any kind of form, layout, modal, button, dropdown, or typography treatment in a WB-enabled codebase, this skill applies — even if they don't explicitly say "Wonder Blocks". Do NOT trigger for debugging TypeScript errors, writing tests, or fixing CI/lint issues in WB packages.
| name | unit-tests |
| description | Jest + React Testing Library best practices for Wonder Blocks unit tests. Use when creating or editing `.test.ts` / `.test.tsx` files. |
This guide covers testing patterns and best practices for Jest and React Testing Library in the Wonder Blocks codebase.
Test Workflow Priority:
Unhandled console.error call messagesUnhandled console.error call, look for the root cause error (e.g., ReferenceError: window is not defined)File Structure:
.test.ts or .test.tsx suffix__tests__/ directory OR colocate with source files (follow local conventions)Test Framework Setup:
jest-extended are availabledescribe/it pattern for test organizationglobalThis prefix when accessing global objects⚠️ ALWAYS use this three-section structure:
describe("Calculator", () => {
it("should add two numbers correctly", () => {
// Arrange
const a = 5;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(8);
});
});
Rules:
// Arrange, // Act, // Assert) — no additional comments within a section// Act & Assert)Exception - Testing Thrown Errors:
When testing errors, use an underTest variable in the Act section:
it("should throw an error when input is invalid", () => {
// Arrange
const invalidInput = "invalid";
// Act
const underTest = () => {
processInput(invalidInput);
};
// Assert
expect(underTest).toThrow("Invalid input");
});
⚠️ Focus on what matters - don't overdo it:
DO Test:
DON'T Test:
// ❌ DON'T: Testing style-only props (use visual regression tests instead)
it("should apply primary color when kind is primary", () => {
render(<Button kind="primary" />);
expect(screen.getByRole("button")).toHaveStyle({ backgroundColor: "blue" });
});
// ✅ DO: Test meaningful behavior
it("should call onClick when clicked", async () => {
// Arrange
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
// Act
await userEvent.click(screen.getByRole("button"));
// Assert
expect(handleClick).toHaveBeenCalledTimes(1);
});
// ✅ DO: Test non-trivial logic
it("should validate email format and return error message", () => {
// Arrange
const invalidEmail = "not-an-email";
// Act
const result = validateEmail(invalidEmail);
// Assert
expect(result).toBe("Please enter a valid email address");
});
Key Principles:
Best Practices:
toBe, toEqual, toHaveBeenCalledWith)toBeInTheDocument(), toBeVisible(), toHaveAttribute().toMatchSnapshot(), .toMatchInlineSnapshot()) - use Chromatic + Storybook for visual regression tests, or use specific attribute assertions instead⚠️ Each test should have exactly one expect. If you need to assert multiple things, split them into separate tests. Multiple assertions hide which behavior actually broke when the test fails.
it.eachWhen to use: Testing the same logic with multiple input/output combinations
✅ DO: Use it.each for data-driven tests
describe("Calculator", () => {
it.each([
[2, 3, 5],
[0, 0, 0],
[-1, 1, 0],
[10, -5, 5],
])("should add %i and %i to equal %i", (a, b, expected) => {
// Arrange
// (inputs come from it.each)
// Act
const result = add(a, b);
// Assert
expect(result).toBe(expected);
});
});
Benefits:
console.error - This hides real implementation issues and errorsjest.spyOn() to create spies - Never treat the original function as though it were a spy✅ DO: Use jest.spyOn and store the result when asserting
import * as SomeFile from "./some-file.ts";
describe("MyComponent", () => {
it("should call someMethod with correct args", () => {
// Arrange
// Store spy because we'll assert on it later
const spy = jest.spyOn(SomeFile, "someMethod").mockReturnValue(mockValue);
// Act
myFunction();
// Assert
expect(spy).toHaveBeenCalledWith(expectedArgs);
});
});
❌ DON'T: Treat the original function as a spy without jest.spyOn()
// ❌ WRONG - This will fail because someMethod is not a spy
import * as SomeFile from "./some-file.ts";
describe("MyComponent", () => {
it("should call someMethod", () => {
// Act
myFunction();
// Assert
expect(SomeFile.someMethod).toHaveBeenCalled(); // ❌ ERROR! Not a spy
});
});
Spies serve two purposes:
✅ Mocking only (no variable needed):
it("should process user data", () => {
// Arrange
// Mock the API call to return test data, but don't store it
jest.spyOn(API, "fetchUser").mockResolvedValue(mockUserData);
// Act
const result = processUserProfile();
// Assert
// We're testing processUserProfile's logic, not that fetchUser was called
expect(result.displayName).toBe("John Doe");
// No spy variable = no unused variable linter error
});
✅ Mocking AND verification (store in variable):
it("should call analytics when button is clicked", () => {
// Arrange
// Store the spy because we'll assert on it
const trackEventSpy = jest
.spyOn(Analytics, "trackEvent")
.mockReturnValue(undefined);
// Act
userEvent.click(screen.getByRole("button"));
// Assert
// We're testing that the analytics call happens correctly
expect(trackEventSpy).toHaveBeenCalledWith("button_click", {
buttonId: "submit",
});
});
Key point: Only store the spy in a variable if you're going to assert on it. This avoids unused variable linter errors while still allowing you to verify calls when needed.
Mock only (no variable):
// When you only need to control the return value
jest.spyOn(module, "functionName").mockReturnValue(mockValue);
jest.spyOn(module, "asyncFunction").mockResolvedValue(mockValue);
jest.spyOn(module, "asyncFunction").mockRejectedValue(new Error("Test error"));
Mock and verify (store in variable):
// When you need to assert the spy was called
const spy = jest.spyOn(module, "functionName").mockReturnValue(mockValue);
// ... later in Assert section:
expect(spy).toHaveBeenCalledWith(expectedArgs);
Spy with mock implementation:
// Store only if you'll verify it was called
const spy = jest.spyOn(module, "functionName").mockImplementation((arg) => {
return processedValue;
});
DO:
jest.spyOn() for mocking functions and tracking calls.mockReturnValue() or similar directly on jest.spyOn() when only mocking behaviorclearAllMocks)DON'T:
console.error - this hides real implementation issuesjest.spyOn()✅ Use renderHook:
import {renderHook} from "@testing-library/react";
// Direct for simple hooks
const {result} = renderHook(() => useMyHook(params));
✅ ALWAYS use userEvent for interactions:
import userEvent from "@testing-library/user-event";
// ✅ DO: Use userEvent (realistic, includes focus/blur/typing)
await userEvent.click(screen.getByRole("button"));
await userEvent.type(screen.getByRole("textbox"), "hello");
// ❌ DON'T: Use fireEvent (low-level, less realistic)
fireEvent.click(button);
⚠️ jsdom does not fully implement all browser behaviors. Common limitations include: getBoundingClientRect(), scroll positions, offsetWidth/offsetHeight, clipboard API, CSS animations, and Intersection/Resize Observers.
✅ Mock browser APIs when testing in unit tests:
// Mock scrollIntoView
const scrollIntoViewMock = jest.fn();
Element.prototype.scrollIntoView = scrollIntoViewMock;
// Mock getBoundingClientRect
jest.spyOn(Element.prototype, "getBoundingClientRect").mockReturnValue({
top: 100, left: 100, bottom: 200, right: 200,
width: 100, height: 100, x: 100, y: 100, toJSON: () => {},
});
✅ Use Storybook interaction tests for behavior that's difficult to mock accurately (scroll, layout, clipboard, complex focus management). See .agents/skills/storybook/SKILL.md for details.
Query priority (use in this order):
getByRole, getByLabelText, getByTextgetByTestId with data-testid attribute❌ NEVER access DOM nodes directly. Always use Testing Library queries. Direct node access couples tests to implementation structure, not behavior.
// ✅ DO: Testing Library queries
screen.getByRole("button", {name: /submit/i});
screen.getByLabelText("Email address");
screen.findByText("Welcome back");
screen.getByTestId("custom-widget");
// ❌ DON'T: CSS selectors
container.querySelector(".my-class");
container.querySelector("#my-id");
// ❌ DON'T: Direct node traversal
element.parentElement;
element.children[0];
element.firstChild;
element.nextSibling;
✅ Group related tests using describe blocks:
describe("MyComponent", () => {
describe("Props", () => { /* prop tests */ });
describe("Event Handlers", () => { /* onClick, onChange tests */ });
describe("Accessibility", () => {
describe("axe", () => { /* toHaveNoA11yViolations tests */ });
describe("ARIA", () => { /* aria attribute tests */ });
describe("Focus", () => { /* focus management tests */ });
describe("Keyboard Interactions", () => { /* keyboard nav tests */ });
});
});
Unit tests for a component should cover:
it.each when there are multiple combinations of things you want to test together.toHaveNoA11yViolations jest matcher to confirm that a component doesn't have accessibility warningsaria-disabled="true" for determining disabled state (not the disabled attribute)# Run all tests
pnpm jest
# Run tests in watch mode
pnpm jest --watch
# Update snapshots
pnpm jest -u
# Run tests with coverage
pnpm jest --coverage
# Run specific test file
pnpm jest path/to/test-file.test.ts
# Debug with verbose output
pnpm jest --verbose --runInBand
Terminal Commands
console.log statements for debugging--verbose flag for detailed output--runInBand for sequential execution (easier to debug)debugger statementsgetByRole, getByLabelText) over test IDsuserEvent instead of fireEventjest.spyOn() and only store in variables when assertingtoHaveNoA11yViolations testsdescribe blocksexpect per test — split into separate tests if you need moreit.each for testing multiple input/output combinationsconsole.error, over-testing trivial code, adding logic to tests