React Testing Library Setup
What We Did
Section titled “What We Did”Added comprehensive React Testing Library (RTL) test coverage for the AuthForm component, establishing patterns for frontend component testing in the Next.js web application.
Why React Testing Library
Section titled “Why React Testing Library”React Testing Library encourages testing components the way users interact with them, rather than testing implementation details. This approach:
- User-centric: Tests query elements by accessible roles, labels, and text
- Resilient: Tests don’t break when refactoring internal component structure
- Confidence: Validates actual user workflows rather than internal state
- Best practices: Enforces accessible markup through its query priorities
Combined with Vitest’s speed and jsdom environment, this provides fast, reliable component tests.
What Was Added
Section titled “What Was Added”Dependencies
Section titled “Dependencies”Added to apps/web/package.json:
{ "devDependencies": { "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.6.1", "@testing-library/jest-dom": "^6.9.1" }}- @testing-library/react: Core RTL with React 19 support
- @testing-library/user-event: Simulates realistic user interactions
- @testing-library/jest-dom: Custom matchers like
toBeInTheDocument()
Vitest Setup File
Section titled “Vitest Setup File”Created apps/web/vitest.setup.ts:
import "@testing-library/jest-dom/vitest"This imports jest-dom matchers globally for all tests.
Vitest Configuration
Section titled “Vitest Configuration”Updated apps/web/vitest.config.ts:
export default defineConfig({ plugins: [react()], ...sharedConfig, test: { ...sharedConfig.test, environment: "jsdom", passWithNoTests: true, setupFiles: ["./vitest.setup.ts"], alias: { "@/": new URL("./", import.meta.url).pathname, }, },})Key changes:
- Added
setupFilesto load jest-dom matchers - Fixed alias path from
./src/to./(web app has no src folder)
Test File Structure
Section titled “Test File Structure”Created apps/web/components/auth-form.test.tsx with 25 tests covering:
Rendering (3 tests)
- Default login mode rendering
- Email and password field presence
- Signup mode when toggled
Mode Switching (2 tests)
- Login to signup transition
- Signup back to login transition
Validation (8 tests)
- Email: required, format validation, valid acceptance
- Password: required, minimum length, valid acceptance
- Name: required in signup mode, valid acceptance
Form Submission (8 tests)
- Login: API call, error handling, default error, loading state
- Signup: API call, error handling, default error, loading state
Error Handling (3 tests)
- Network errors
- Non-Error thrown objects
- Error clearing on mode switch
Input States (1 test)
- Disabled during loading
Testing Patterns
Section titled “Testing Patterns”Mocking External Dependencies
Section titled “Mocking External Dependencies”vi.mock("@/lib/auth-client", () => ({ authClient: { signIn: { email: vi.fn() }, signUp: { email: vi.fn() }, },}))User Interactions with userEvent
Section titled “User Interactions with userEvent”const user = userEvent.setup()await user.type(screen.getByLabelText("Email"), "test@example.com")await user.click(screen.getByRole("button", { name: "Sign in" }))Async Validation with waitFor
Section titled “Async Validation with waitFor”await waitFor(() => { expect(screen.getByText("Email is required")).toBeInTheDocument()})Testing onChange Validation
Section titled “Testing onChange Validation”The AuthForm uses TanStack Form with onChange validators. To test “required” validation, you must type and then clear the field:
await user.type(emailInput, "a")await user.clear(emailInput)await user.tab()
await waitFor(() => { expect(screen.getByText("Email is required")).toBeInTheDocument()})Testing Loading States
Section titled “Testing Loading States”let resolvePromise: (value: unknown) => voidvi.mocked(authClient.signIn.email).mockReturnValue( new Promise((resolve) => { resolvePromise = resolve }) as never)
// Trigger submissionawait user.click(screen.getByRole("button", { name: "Sign in" }))
// Assert loading stateexpect(screen.getByRole("button", { name: "Signing in..." })).toBeDisabled()
// Clean upresolvePromise!({ data: {} })Running Tests
Section titled “Running Tests”# Run web app teststurbo test --filter=web
# Watch modeturbo test:watch --filter=web
# Run all tests in monorepoturbo testImportant Notes
Section titled “Important Notes”Query Priority
Section titled “Query Priority”Follow RTL’s query priority:
getByRole- Accessible queries (preferred)getByLabelText- Form fieldsgetByPlaceholderText- When label isn’t availablegetByText- Non-interactive elementsgetByTestId- Last resort
Act Warnings
Section titled “Act Warnings”Some tests may show act() warnings when testing loading states with unresolved promises. These are warnings, not failures, and occur because the promise resolves after the test completes.
Don’t Use bun test
Section titled “Don’t Use bun test”The web app tests require Vitest’s jsdom environment. Running bun test directly will fail because Bun’s native test runner doesn’t set up jsdom. Always use turbo test or vitest run.