--- 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:** ```typescript 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:** - ✅ **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: ```typescript it("should throw an error when input is invalid", () => { // Arrange const invalidInput = "invalid"; // Act const underTest = () => { processInput(invalidInput); }; // Assert 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 ```typescript // ❌ DON'T: Testing style-only props (use visual regression tests instead) it("should apply primary color when kind is primary", () => { render(); // 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:** - ✅ **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** ```typescript 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:** - ✅ **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 1. **NEVER mock `console.error`** - This hides real implementation issues and errors 2. **ALWAYS use `jest.spyOn()` to create spies** - Never treat the original function as though it were a spy 3. **Store spy return values in variables ONLY when asserting on them** - Avoids unused variable linter errors 4. **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** ```typescript 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()** ```typescript // ❌ 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 }); }); ``` ### When to Store Spies in Variables **Spies serve two purposes:** 1. **Mocking behavior** - Replace function implementation or return value 2. **Verification** - Assert the function was called with correct arguments **✅ Mocking only (no variable needed):** ```typescript 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):** ```typescript 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. ### Common Spy Patterns **Mock only (no variable):** ```typescript // 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):** ```typescript // 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:** ```typescript // Store only if you'll verify it was called 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`:** ```typescript import {renderHook} from "@testing-library/react"; // Direct for simple hooks const {result} = renderHook(() => useMyHook(params)); ``` ### User Interactions **✅ ALWAYS use `userEvent` for interactions:** ```typescript 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); ``` ### 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:** ```typescript // 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. ### Element Selection **Query priority (use in this order):** 1. ✅ **Semantic queries** (best): `getByRole`, `getByLabelText`, `getByText` 2. ✅ **Test IDs** (fallback): `getByTestId` with `data-testid` attribute 3. ❌ **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. ```typescript // ✅ 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; ``` ## Wonder Blocks Component Testing ### Test Organization **✅ Group related tests using `describe` blocks:** ```typescript 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 */ }); }); }); ``` ### Test Coverage Unit tests for a component should cover: #### Base Tests - ref is forwarded #### 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 ```bash # 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 ``` ### 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 1. ✅ **Structure**: Use Arrange-Act-Assert pattern with comments 2. ✅ **Focus**: Test behavior, not implementation details 3. ✅ **Queries**: Use semantic queries (`getByRole`, `getByLabelText`) over test IDs 4. ✅ **Interactions**: Use `userEvent` instead of `fireEvent` 5. ✅ **Spies**: Use `jest.spyOn()` and only store in variables when asserting 6. ✅ **Accessibility**: Include `toHaveNoA11yViolations` tests 7. ✅ **Organization**: Group related tests with `describe` blocks 8. ✅ **Assertions**: One `expect` per test — split into separate tests if you need more 9. ✅ **Parameterized**: Use `it.each` for testing multiple input/output combinations 10. ✅ **Browser APIs**: Mock jsdom limitations properly, or use Storybook interaction tests for browser-specific behavior 11. ✅ **Bug Fixes**: Add tests that reproduce bugs to prevent regressions 12. ❌ **Avoid**: Testing style-only props, mocking `console.error`, over-testing trivial code, adding logic to tests