# Testing Studio React Components https://www.sanity.io/learn/course/testing-sanity-studio/testing-studio-react-component.md Test custom input components that render UI, use Studio hooks, and handle user interactions. Configure a browser-like test environment with React Testing Library, create provider fixtures that supply Studio context, and verify component behavior through simulated user actions. Learn what aspects of components are worth testing and how to balance thorough coverage with maintainable tests. The validation logic and access control functions you've tested so far run in Node.js with no UI. Custom input components are different—they render DOM elements, depend on Sanity UI's theming context, use Studio hooks like `useFormValue` and `useClient`, and respond to user interactions. Testing these components requires a browser-like environment, provider setup for Sanity UI, and tools to simulate user behavior like clicks and typing. Testing components verifies not just that your code runs, but that content editors can successfully interact with your custom inputs. A permission check might pass its pure function test but fail when integrated into a component's `readOnly` callback. A date calculation might work in isolation but render incorrectly when formatted for display. Component tests catch these integration issues by exercising the full user interaction flow. ## Setting up the component testing environment Install React Testing Library and `jsdom`: ```sh pnpm add -D @testing-library/react @testing-library/user-event jsdom @testing-library/jest-dom ``` These packages provide: * **`jsdom`** - A browser environment implementation for Node.js * **@testing-library/react** - Utilities for rendering and querying React components * **@testing-library/user-event** - Functions that simulate realistic user interactions * **@testing-library/jest-dom** - Additional matchers for DOM testing 1. `jsdom` simulates a browser environment in Node.js. It's not a real browser, so some browser-specific features (like layout calculations) won't work. For most Studio components, `jsdom` is sufficient. ## Configuring multiple test environments Your test suite now has different needs: pure functions run in Node, React components need a browser environment. Update `apps/studio/vitest.config.ts` to define **test-type projects** (not to be confused with workspace projects): ```typescript:apps/studio/vitest.config.ts import {defineProject} from 'vitest/config' export default defineProject({ test: { name: 'studio', // Define test-type projects within the Studio workspace project projects: [ { test: { name: 'unit', environment: 'node', include: ['**/*.test.ts'], }, }, { test: { name: 'component', environment: 'jsdom', include: ['**/*.test.tsx'], setupFiles: ['./__tests__/setup.ts'], }, }, ], }, }) ``` 1. **Important:** Vitest requires unique project names across your entire workspace. If you add other workspace projects, you may need to adjust the naming convention. And we'll need to create the setup file for the component test suite: ```typescript:apps/studio/__tests__/setup.ts import {cleanup} from '@testing-library/react' import {afterEach} from 'vitest' import '@testing-library/jest-dom/vitest' afterEach(() => { cleanup() }) ``` 1. Component tests are slower than unit tests because they render real DOM elements. This is why we separate them into different test projects with the `.test.tsx` extension. This configuration creates **nested projects**: 1. **Workspace project** - `studio` (defined at root `vitest.config.ts` with `projects: ['apps/*']`) 2. **Test-type projects** - `unit` and `component` (defined within Studio's config) The `component` project: * Uses `jsdom` to provide DOM APIs that React needs * Only runs files with `.test.tsx` extension (TypeScript with JSX) * Runs a setup file before each test ## Creating provider fixtures React components in Sanity Studio don't work in isolation—they expect certain contexts to exist. Sanity UI components read theme values from a `ThemeProvider` to render correctly (colors, spacing, typography). Without this provider, components throw errors or render incorrectly. Similarly, components that trigger toast notifications need a `ToastProvider`, and dialogs need a `LayerProvider` for z-index management. In production, your Studio wraps everything with these providers automatically. In tests, you need to recreate this harness manually. Rather than wrap each component individually in every test, create a reusable fixture that provides all required contexts: ```tsx:apps/studio/__tests__/fixtures/providers.tsx import {LayerProvider, ThemeProvider, ToastProvider} from '@sanity/ui' import {defaultTheme} from 'sanity' import {render, type RenderOptions} from '@testing-library/react' import type {ReactElement} from 'react' export function TestProviders({children}: {children: React.ReactNode}) { return ( {children} ) } export function renderWithProviders( ui: ReactElement, options?: Omit ) { return render(ui, {wrapper: TestProviders, ...options}) } ``` 1. Start with these three providers for most Sanity UI components. Add more providers (like `FormBuilderProvider`) only when your components require them. Keep provider setup minimal. The `TestProviders` component recreates the essential Studio provider hierarchy for these component tests: * **`ThemeProvider`** - Provides theme values (colors, spacing, typography) * **`ToastProvider`** - Enables toast notifications (some inputs use these) * **`LayerProvider`** - Manages z-index stacking for dialogs and popovers These are the providers the `DoorsOpenInput` component needs. Other custom components might require additional providers—like `FormBuilderProvider` for components that use form-level hooks, or custom context providers for specialized features. Add providers to `TestProviders` as your test suite requires them. The `renderWithProviders()` helper wraps React Testing Library's `render()` function, automatically including these providers for every component test. This fixture becomes your test harness for Sanity UI components—call it instead of `render()` and components work as they would in production. ## Testing the `DoorsOpenInput` component The `DoorsOpenInput` component shows when doors open based on the event date and a minutes-before value. It uses: * `useFormValue` hook to read the event date * `useClient` hook to access the Sanity client * Custom logic to calculate and display the doors open time ## Testing the `DoorsOpenInput` component The `DoorsOpenInput` component displays when doors open for an event, calculated from the event date and a minutes-before value: ```tsx:apps/studio/schemaTypes/components/DoorsOpenInput.tsx import {getPublishedId, type NumberInputProps, useClient, useFormValue} from 'sanity' import {Stack, Text} from '@sanity/ui' import {useEffect} from 'react' // .. export function DoorsOpenInput(props: NumberInputProps) { const date = useFormValue(['date']) as string | undefined const client = useClient({apiVersion: '2025-05-08'}) const documentId = useFormValue(['_id']) as string | undefined // Query for document versions (demonstration purposes) useEffect(() => { if (!documentId) return // ... query logic }, [client, documentId]) return ( {props.renderDefault(props)} {typeof props.value === 'number' && date ? ( Doors open{' '} {subtractMinutesFromDate(date, props.value).toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', })} ) : null} ) } ``` The component uses Sanity hooks (`useFormValue`, `useClient`) to access form data and the Sanity client. Testing it requires mocking these hooks to control what values the component receives: ```tsx:apps/studio/schemaTypes/components/DoorsOpenInput.test.tsx import {describe, it, expect, vi, beforeEach} from 'vitest' import {screen} from '@testing-library/react' import {useFormValue, useClient} from 'sanity' import {renderWithProviders} from '../../../__tests__/fixtures/providers' import {DoorsOpenInput} from './DoorsOpenInput' // Mock Sanity hooks vi.mock('sanity', async () => { const actual = await vi.importActual('sanity') return { ...actual, useFormValue: vi.fn(), useClient: vi.fn(), } }) describe('DoorsOpenInput', () => { const mockProps = { value: 60, onChange: vi.fn(), renderDefault: vi.fn((props) => ), } as any beforeEach(() => { vi.clearAllMocks() }) it('shows doors open time when date and value exist', () => { vi.mocked(useFormValue).mockImplementation((path) => { if (path[0] === 'date') return '2025-06-15T20:00:00Z' if (path[0] === '_id') return 'event-1' return undefined }) vi.mocked(useClient).mockReturnValue({} as any) renderWithProviders() expect(screen.getByText(/Doors open/i)).toBeInTheDocument() expect(screen.getByText(/June/i)).toBeInTheDocument() }) it('hides doors open time when date is missing', () => { vi.mocked(useFormValue).mockImplementation((path) => { if (path[0] === 'date') return undefined if (path[0] === '_id') return 'event-2' return undefined }) vi.mocked(useClient).mockReturnValue({} as any) renderWithProviders() expect(screen.queryByText(/Doors open/i)).not.toBeInTheDocument() }) it('hides doors open time when value is missing', () => { vi.mocked(useFormValue).mockImplementation((path) => { if (path[0] === 'date') return '2025-06-15T20:00:00Z' if (path[0] === '_id') return 'event-3' return undefined }) vi.mocked(useClient).mockReturnValue({} as any) const propsWithoutValue = {...mockProps, value: undefined} renderWithProviders() expect(screen.queryByText(/Doors open/i)).not.toBeInTheDocument() }) }) ``` 1. Module mocks apply to the entire test file. If you need different mock behavior per test, use `mockImplementation()` in each test's setup rather than at the file level. This test mocks the entire `sanity` module to control what `useFormValue` and `useClient` return: 1. **`vi.mock('sanity')`** - Intercepts all imports from the `sanity` package 2. **`vi.importActual()`** - Preserves original exports (types, utilities) 3. **`useFormValue: vi.fn()`** - Replaces hook with mock function 4. **`mockImplementation()`** - Controls what the hook returns based on the path argument 5. **`beforeEach()`** - Clears mocks between tests for isolation The `mockImplementation()` function lets you return different values based on which form field is being accessed. When the component calls `useFormValue(['date'])`, the mock checks if `path[0] === 'date'` and returns the appropriate value. This pattern works for any component that reads multiple form fields through `useFormValue`. ## What to test in components Focus on behavior that matters to content editors and enforced data integrity: **Test:** * Components render with expected content * User interactions trigger correct callbacks * Interactive components mutate documents accurately * Accessibility attributes are present **Don't test:** * Implementation details (state variable names, hooks used) * Sanity internals (we ensure test coverage for our own code) * CSS styles and exact positioning * Third-party library behavior (they should be testing it) Custom inputs that modify documents deserve thorough testing. A bug in a read-only component might confuse an editor temporarily, but a bug in a component that writes data to the published document rather than the current draft would be a headache. Test these rigorously to ensure they write the correct values to the intended fields in the intended document. 1. Add a test that verifies the component calls `onChange` when the user modifies the input value. Use `userEvent.type()` to simulate typing. ## Next steps You've learned to test React components in Sanity Studio: * Setting up `jsdom` for browser-like testing * Creating provider fixtures for Studio context * Rendering components with React Testing Library * Simulating user interactions with `userEvent` * Mocking Sanity hooks like `useFormValue` and `useClient` * Using accessible queries to find elements In the next lesson, you'll integrate tests into GitHub Actions, implement coverage reporting, and develop a testing strategy that scales with your Studio.