| 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.
|
Jest Testing Best Practices
This guide covers testing patterns and best practices for Jest and React Testing Library in the Wonder Blocks codebase.
Core Testing Principles
⚠️ Critical Setup Rules
Test Workflow Priority:
- ✅ ALWAYS fix failing tests BEFORE fixing linting errors
- ✅ Focus on underlying errors, not
Unhandled console.error call messages
- ⚠️ When tests fail with
Unhandled console.error call, look for the root cause error (e.g., ReferenceError: window is not defined)
- ⚠️ The console.error messages are symptoms, not the actual problem - fix the underlying issue
File Structure:
- ✅ Name test files with
.test.ts or .test.tsx suffix
- ✅ Place in
__tests__/ directory OR colocate with source files (follow local conventions)
Test Framework Setup:
- ✅ Additional matchers from React Testing Library (RTL) and
jest-extended are available
- ✅ Use
describe/it pattern for test organization
- ✅ Use
globalThis prefix when accessing global objects
- ✅ Prioritize testing non-trivial business logic over trivial implementations
Arrange-Act-Assert Pattern
⚠️ ALWAYS use this three-section structure:
describe("Calculator", () => {
it("should add two numbers correctly", () => {
const a = 5;
const b = 3;
const result = add(a, b);
expect(result).toBe(8);
});
});
Rules:
- ✅ ALWAYS divide tests into Arrange, Act, Assert sections with comments
- ✅ Each section gets exactly one comment label (
// Arrange, // Act, // Assert) — no additional comments within a section
- ❌ NEVER combine sections (e.g., don't write
// Act & Assert)
- ❌ NEVER use multiple Act or Assert sections in a single test (split into separate tests instead)
- ❌ NEVER remove Arrange, Act, Assert comments
Exception - Testing Thrown Errors:
When testing errors, use an underTest variable in the Act section:
it("should throw an error when input is invalid", () => {
const invalidInput = "invalid";
const underTest = () => {
processInput(invalidInput);
};
expect(underTest).toThrow("Invalid input");
});
Be Concise and Avoid Over-Testing
⚠️ Focus on what matters - don't overdo it:
DO Test:
- ✅ Non-trivial business logic - Complex calculations, data transformations, validation rules
- ✅ User interactions - Click handlers, form submissions, keyboard navigation
- ✅ Accessibility - ARIA attributes, keyboard support, focus management
- ✅ Edge cases and error conditions - Null values, empty states, error handling
- ✅ Integration points - API calls, event callbacks, state changes
- ✅ Bug fixes - Add a test that reproduces the bug to prevent regressions
DON'T Test:
- ❌ Trivial implementations - Simple getters/setters, pass-through functions
- ❌ Style-only props - Visual appearance is covered by visual regression tests in Storybook
- ❌ Third-party libraries - Assume they work; test your usage of them
- ❌ Implementation details - Internal state that doesn't affect output/behavior
- ❌ Additional logic in tests - Use existing utility functions instead of reimplementing logic in tests
it("should apply primary color when kind is primary", () => {
render(<Button kind="primary" />);
expect(screen.getByRole("button")).toHaveStyle({ backgroundColor: "blue" });
});
it("should call onClick when clicked", async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await userEvent.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("should validate email format and return error message", () => {
const invalidEmail = "not-an-email";
const result = (invalidEmail);
(result).();
});
Key Principles:
- ✅ Test behavior, not implementation - Focus on what the component does, not how
- ✅ Prioritize critical paths - Test the most important user flows first
- ✅ Keep tests simple and readable - Each test should have a clear, single purpose
- ✅ Don't add logic to tests - Tests should only test the component/function; use existing utility functions from the codebase instead of reimplementing logic in tests
- ✅ Use visual regression tests for styling - Storybook snapshot tests handle visual appearance
- ✅ Balance coverage with maintainability - More tests ≠ better tests
Assertions
Best Practices:
- ✅ Use specific matchers when possible (e.g.,
toBe, toEqual, toHaveBeenCalledWith)
- ✅ Prefer explicit assertions over implicit ones
- ✅ Use semantic matchers from RTL:
toBeInTheDocument(), toBeVisible(), toHaveAttribute()
- ❌ Avoid Jest snapshots (
.toMatchSnapshot(), .toMatchInlineSnapshot()) - use Chromatic + Storybook for visual regression tests, or use specific attribute assertions instead
One Expect Per Test
⚠️ 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.
Parameterized Tests with it.each
When 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) => {
const result = add(a, b);
expect(result).toBe(expected);
});
});
Benefits:
- ✅ Reduces Duplication: Test same logic with different inputs
- ✅ Clear Test Names: Each test shows specific values being tested
- ✅ Easy to Extend: Simply add new arrays to test data
- ✅ Better Coverage: Test edge cases and boundary conditions efficiently
- ✅ Comprehensive Testing: Essential for testing all prop combinations and states in Wonder Blocks components
Mocking and Spying
⚠️ Critical Rules - ALWAYS Follow These
- NEVER mock
console.error - This hides real implementation issues and errors
- ALWAYS use
jest.spyOn() to create spies - Never treat the original function as though it were a spy
- Store spy return values in variables ONLY when asserting on them - Avoids unused variable linter errors
- NEVER mock outside of tests - Even if it means code duplication, keep mocks inside test cases
Method Spying - Correct Pattern
✅ 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", () => {
const spy = jest.spyOn(SomeFile, "someMethod").mockReturnValue(mockValue);
myFunction();
expect(spy).toHaveBeenCalledWith(expectedArgs);
});
});
❌ DON'T: Treat the original function as a spy without jest.spyOn()
import * as SomeFile from "./some-file.ts";
describe("MyComponent", () => {
it("should call someMethod", () => {
myFunction();
expect(SomeFile.someMethod).toHaveBeenCalled();
});
});
When to Store Spies in Variables
Spies serve two purposes:
- Mocking behavior - Replace function implementation or return value
- Verification - Assert the function was called with correct arguments
✅ Mocking only (no variable needed):
it("should process user data", () => {
jest.spyOn(API, "fetchUser").mockResolvedValue(mockUserData);
const result = processUserProfile();
expect(result.displayName).toBe("John Doe");
});
✅ Mocking AND verification (store in variable):
it("should call analytics when button is clicked", () => {
const trackEventSpy = jest
.spyOn(Analytics, "trackEvent")
.mockReturnValue(undefined);
userEvent.click(screen.getByRole("button"));
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.
Common Spy Patterns
Mock only (no variable):
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):
const spy = jest.spyOn(module, "functionName").mockReturnValue(mockValue);
expect(spy).toHaveBeenCalledWith(expectedArgs);
Spy with mock implementation:
const spy = jest.spyOn(module, "functionName").mockImplementation((arg) => {
return processedValue;
});
Mocking Guidelines
DO:
- ✅ Use
jest.spyOn() for mocking functions and tracking calls
- ✅ Store spy return values in variables ONLY when you need to assert on them
- ✅ Chain
.mockReturnValue() or similar directly on jest.spyOn() when only mocking behavior
- ✅ Keep mocks and spies inside test cases when possible
- ✅ Use mocking and spies to isolate the code under test at boundaries with other code
- ✅ Clean up spies after tests (Jest does this automatically with
clearAllMocks)
DON'T:
- ❌ Never mock
console.error - this hides real implementation issues
- ❌ Never treat original functions as spies without
jest.spyOn()
- ❌ Never store spies in variables if you won't assert on them (causes unused variable linter errors)
- ❌ Avoid mocking outside of tests, even if it means code duplication
Hook Testing
✅ Use renderHook:
import {renderHook} from "@testing-library/react";
const {result} = renderHook(() => useMyHook(params));
User Interactions
✅ ALWAYS use userEvent for interactions:
import userEvent from "@testing-library/user-event";
await userEvent.click(screen.getByRole("button"));
await userEvent.type(screen.getByRole("textbox"), "hello");
fireEvent.click(button);
Browser Behavior and jsdom Limitations
⚠️ 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:
const scrollIntoViewMock = jest.fn();
Element.prototype.scrollIntoView = scrollIntoViewMock;
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.
Element Selection
Query priority (use in this order):
- ✅ Semantic queries (best):
getByRole, getByLabelText, getByText
- ✅ Test IDs (fallback):
getByTestId with data-testid attribute
- ❌ NEVER use CSS selectors, direct DOM traversal, or raw node access
❌ NEVER access DOM nodes directly. Always use Testing Library queries. Direct node access couples tests to implementation structure, not behavior.
screen.getByRole("button", {name: /submit/i});
screen.getByLabelText("Email address");
screen.findByText("Welcome back");
screen.getByTestId("custom-widget");
container.querySelector(".my-class");
container.querySelector("#my-id");
element.parentElement;
element.children[0];
element.firstChild;
element.nextSibling;
Wonder Blocks Component Testing
Test Organization
✅ Group related tests using describe blocks:
describe("MyComponent", () => {
describe("Props", () => { });
describe("Event Handlers", () => { });
describe("Accessibility", () => {
describe("axe", () => { });
describe("ARIA", () => { });
describe("Focus", () => { });
describe("Keyboard Interactions", () => { });
});
});
Test Coverage
Unit tests for a component should cover:
Base Tests
Props
- Cover expected behaviour when certain props are set.
- Exclude tests for props that are related to styles only, since this should be covered by visual regression tests instead
- Cover expected behaviour with default prop values
- Use
it.each when there are multiple combinations of things you want to test together
Event Handlers
- Check that any event handlers are triggered by the expected conditions
- Verify callbacks are called with correct arguments
Accessibility
- Confirm that roles, semantics, and aria attributes are correctly set and wired together
- Use the
.toHaveNoA11yViolations jest matcher to confirm that a component doesn't have accessibility warnings
- Confirm keyboard interactions and navigation
- Focus management
- Confirm accessible names
- Check for
aria-disabled="true" for determining disabled state (not the disabled attribute)
Tools and Commands
Terminal Commands
pnpm jest
pnpm jest --watch
pnpm jest -u
pnpm jest --coverage
pnpm jest path/to/test-file.test.ts
pnpm jest --verbose --runInBand
Debugging Priority Order
Terminal Commands
- Add
console.log statements for debugging
- Use
--verbose flag for detailed output
- Use
--runInBand for sequential execution (easier to debug)
- Use Node debugger with
debugger statements
Best Practices Summary
- ✅ Structure: Use Arrange-Act-Assert pattern with comments
- ✅ Focus: Test behavior, not implementation details
- ✅ Queries: Use semantic queries (
getByRole, getByLabelText) over test IDs
- ✅ Interactions: Use
userEvent instead of fireEvent
- ✅ Spies: Use
jest.spyOn() and only store in variables when asserting
- ✅ Accessibility: Include
toHaveNoA11yViolations tests
- ✅ Organization: Group related tests with
describe blocks
- ✅ Assertions: One
expect per test — split into separate tests if you need more
- ✅ Parameterized: Use
it.each for testing multiple input/output combinations
- ✅ Browser APIs: Mock jsdom limitations properly, or use Storybook interaction tests for browser-specific behavior
- ✅ Bug Fixes: Add tests that reproduce bugs to prevent regressions
- ❌ Avoid: Testing style-only props, mocking
console.error, over-testing trivial code, adding logic to tests