Skip to Content

Testing Guide

Learn how to test Hero Hook Form components and forms effectively using React Testing Library, Cypress Component Testing, and Cypress E2E testing.

Overview

Hero Hook Form provides multiple testing approaches:

  • Unit/Component Testing - Test individual components with React Testing Library
  • Cypress Component Testing - Test components in isolation with Cypress
  • Cypress E2E Testing - Test complete form workflows with Cypress helpers
  • Accessibility Testing - Ensure forms are accessible
  • Performance Testing - Verify form performance

Unit/Component Testing with React Testing Library

Basic Component Tests

Test individual field components:

// InputField.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { InputField } from '@rachelallyson/hero-hook-form'; import { useForm, FormProvider } from 'react-hook-form'; function TestWrapper({ children }: { children: React.ReactNode }) { const form = useForm(); return ( <FormProvider {...form}> {children} </FormProvider> ); } describe('InputField', () => { it('renders with label', () => { render( <TestWrapper> <InputField name="test" label="Test Field" /> </TestWrapper> ); expect(screen.getByLabelText('Test Field')).toBeInTheDocument(); }); it('shows validation error', async () => { const form = useForm({ mode: 'onChange', }); render( <FormProvider {...form}> <InputField name="email" label="Email" rules={{ required: 'Email is required', pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Invalid email' } }} /> </FormProvider> ); const input = screen.getByLabelText('Email'); fireEvent.change(input, { target: { value: 'invalid-email' } }); fireEvent.blur(input); await screen.findByText('Invalid email'); }); });

Form Integration Tests

Test complete forms:

// ContactForm.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ZodForm, FormFieldHelpers } from '@rachelallyson/hero-hook-form'; import { z } from 'zod'; const contactSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Invalid email address'), message: z.string().min(10, 'Message must be at least 10 characters'), }); function ContactForm({ onSubmit }: { onSubmit: (data: any) => void }) { return ( <ZodForm config={{ schema: contactSchema, fields: [ FormFieldHelpers.input('name', 'Name'), FormFieldHelpers.input('email', 'Email', 'email'), FormFieldHelpers.textarea('message', 'Message'), ], onSubmit, title: 'Contact Us', }} /> ); } describe('ContactForm', () => { it('submits form with valid data', async () => { const handleSubmit = jest.fn(); render(<ContactForm onSubmit={handleSubmit} />); fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); fireEvent.change(screen.getByLabelText('Message'), { target: { value: 'This is a test message' } }); fireEvent.click(screen.getByRole('button', { name: /submit/i })); await waitFor(() => { expect(handleSubmit).toHaveBeenCalledWith({ name: 'John Doe', email: 'john@example.com', message: 'This is a test message', }); }); }); });

Cypress Component Testing

Setup

Install Cypress and configure component testing:

npm install --save-dev cypress @cypress/react

Add to cypress.config.ts:

import { defineConfig } from 'cypress'; export default defineConfig({ component: { devServer: { framework: 'react', bundler: 'vite', }, }, });

Component Tests

Test components in isolation:

// InputField.cy.tsx import { InputField } from '@rachelallyson/hero-hook-form'; import { useForm, FormProvider } from 'react-hook-form'; function TestWrapper({ children }: { children: React.ReactNode }) { const form = useForm(); return ( <FormProvider {...form}> {children} </FormProvider> ); } describe('InputField', () => { it('renders with label', () => { cy.mount( <TestWrapper> <InputField name="test" label="Test Field" /> </TestWrapper> ); cy.get('label').should('contain', 'Test Field'); cy.get('[name="test"]').should('be.visible'); }); it('shows validation error', () => { cy.mount( <TestWrapper> <InputField name="email" label="Email" rules={{ required: 'Email is required', pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Invalid email' } }} /> </TestWrapper> ); cy.get('[name="email"]').type('invalid-email'); cy.get('[name="email"]').blur(); cy.contains('Invalid email').should('be.visible'); }); });

Cypress E2E Testing

Setup

Add to your cypress/support/e2e.ts:

// Import all Cypress helpers import '@rachelallyson/hero-hook-form/cypress'; // Or import manually for more control import { registerHeroFormCommands } from '@rachelallyson/hero-hook-form/cypress'; registerHeroFormCommands();

Add TypeScript support to cypress/support/index.d.ts:

/// <reference types="@rachelallyson/hero-hook-form/cypress" />

Field Interaction Helpers

Input Fields

// Fill input by type cy.fillInputByType('email', 'test@example.com'); cy.fillInputByType('password', 'securePassword123'); // Fill input by label cy.fillInputByLabel('First Name', 'John'); cy.fillInputByLabel('Email Address', 'john@example.com'); // Fill input by placeholder cy.fillInputByPlaceholder('Enter your email', 'test@example.com'); // Fill textarea cy.fillTextarea('This is a long message...');

Selection Fields

// Select dropdown by label cy.selectDropdownByLabel('Country', 'United States'); // Select dropdown by option value cy.selectDropdownOption('US'); // Radio group selection cy.selectRadioByLabel('Gender', 'Male');

Boolean Fields

// Checkboxes cy.checkCheckboxByLabel('Terms and Conditions'); cy.checkCheckbox(0); // First checkbox // Uncheck checkboxes cy.uncheckCheckboxByLabel('Marketing emails'); // Switches cy.checkSwitchByLabel('Enable notifications'); cy.uncheckSwitchByLabel('Dark mode');

Validation Testing

// Expect specific validation errors cy.expectValidationError('Email is required'); cy.expectFieldError('Email', 'Invalid email address'); // Expect no validation errors cy.expectNoValidationErrors(); // Check if field is valid cy.expectFieldValid('Email'); // Trigger validation cy.triggerValidation();

Form Submission

// Submit form cy.submitForm(); // Submit and expect success cy.submitAndExpectSuccess(); cy.submitAndExpectSuccess('Thank you for your submission'); // Submit and expect errors cy.submitAndExpectErrors(); // Intercept form submission cy.interceptFormSubmission('POST', '/api/contact', 'contactSubmission'); cy.wait('@contactSubmission');

Complete Form Testing

// Fill entire form with data object cy.fillCompleteForm({ firstName: 'John', lastName: 'Doe', email: 'john@example.com', phone: '+1-555-123-4567', country: 'United States', agreeToTerms: true, message: 'This is a test message' }); // Test complex form flows cy.testFormFlow([ { action: 'fill', field: 'firstName', value: 'John' }, { action: 'fill', field: 'email', value: 'john@example.com' }, { action: 'check', field: 'agreeToTerms' }, { action: 'submit' }, { action: 'expectSuccess', message: 'Registration successful' } ]);

Dynamic Forms Testing

Conditional Fields

describe('Conditional Fields', () => { it('shows phone field when checkbox is checked', () => { cy.visit('/contact-form'); // Initially phone field should not exist cy.get('[name="phone"]').should('not.exist'); // Check the "has phone" checkbox cy.checkCheckboxByLabel('I have a phone number'); // Phone field should now be visible cy.get('[name="phone"]').should('be.visible'); // Fill the phone field cy.fillInputByLabel('Phone Number', '+1-555-123-4567'); }); });

Field Arrays

describe('Field Arrays', () => { it('adds and removes items', () => { cy.visit('/shopping-cart'); // Add first item cy.get('[data-testid="add-item"]').click(); cy.fillInputByLabel('Item Name', 'Product 1'); // Add second item cy.get('[data-testid="add-item"]').click(); cy.fillInputByLabel('Item Name', 'Product 2'); // Remove first item cy.get('[data-testid="remove-item-0"]').click(); cy.get('[name="items.0.name"]').should('not.exist'); }); });

Accessibility Testing

Screen Reader Testing

import { render } from '@testing-library/react'; import { axe, toHaveNoViolations } from 'jest-axe'; import { ZodForm, FormFieldHelpers } from '@rachelallyson/hero-hook-form'; import { z } from 'zod'; expect.extend(toHaveNoViolations); describe('Accessibility', () => { it('should not have accessibility violations', async () => { const { container } = render( <ZodForm config={{ schema: z.object({ name: z.string().min(2), email: z.string().email(), }), fields: [ FormFieldHelpers.input('name', 'Name'), FormFieldHelpers.input('email', 'Email', 'email'), ], onSubmit: jest.fn(), }} /> ); const results = await axe(container); expect(results).toHaveNoViolations(); }); });

Keyboard Navigation Testing

it('should navigate fields with Tab key', () => { render( <ZodForm config={{ schema: z.object({ name: z.string(), email: z.string(), }), fields: [ FormFieldHelpers.input('name', 'Name'), FormFieldHelpers.input('email', 'Email'), ], onSubmit: jest.fn(), }} /> ); const nameInput = screen.getByLabelText('Name'); const emailInput = screen.getByLabelText('Email'); nameInput.focus(); expect(nameInput).toHaveFocus(); fireEvent.keyDown(nameInput, { key: 'Tab' }); expect(emailInput).toHaveFocus(); });

Performance Testing

Render Performance

it('should render within performance budget', () => { const start = performance.now(); render( <ZodForm config={{ schema: z.object({ name: z.string(), email: z.string(), }), fields: [ FormFieldHelpers.input('name', 'Name'), FormFieldHelpers.input('email', 'Email'), ], onSubmit: jest.fn(), }} /> ); const end = performance.now(); const renderTime = end - start; expect(renderTime).toBeLessThan(100); // 100ms budget });

Testing Utilities

Custom Test Utilities

// test-utils.tsx import { render, RenderOptions } from '@testing-library/react'; import { FormProvider, useForm } from 'react-hook-form'; import { HeroHookFormProvider } from '@rachelallyson/hero-hook-form'; function AllTheProviders({ children }: { children: React.ReactNode }) { const form = useForm(); return ( <HeroHookFormProvider> <FormProvider {...form}> {children} </FormProvider> </HeroHookFormProvider> ); } const customRender = ( ui: React.ReactElement, options?: Omit<RenderOptions, 'wrapper'> ) => render(ui, { wrapper: AllTheProviders, ...options }); export * from '@testing-library/react'; export { customRender as render };

Best Practices

1. Test User Interactions

// âś… Good: Test user interactions it('should submit form when user clicks submit button', () => { render(<MyForm />); fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); fireEvent.click(screen.getByRole('button', { name: /submit/i })); expect(mockSubmit).toHaveBeenCalledWith({ name: 'John Doe', }); });

2. Use Semantic Queries

// ✅ Good: Use semantic queries screen.getByRole('button', { name: /submit/i }); screen.getByLabelText('Email'); screen.getByText('Form submitted successfully'); // ❌ Bad: Use implementation queries screen.getByTestId('submit-button'); screen.getByClassName('email-input');

3. Test Error States

// âś… Good: Test error states it('should show validation errors', async () => { render(<MyForm />); fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'invalid-email' } }); fireEvent.blur(screen.getByLabelText('Email')); await screen.findByText('Invalid email address'); });

4. Test Loading States

// âś… Good: Test loading states it('should show loading state during submission', async () => { const slowSubmit = jest.fn().mockImplementation( () => new Promise(resolve => setTimeout(resolve, 1000)) ); render(<MyForm onSubmit={slowSubmit} />); fireEvent.click(screen.getByRole('button', { name: /submit/i })); expect(screen.getByText('Submitting...')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('Form submitted successfully')).toBeInTheDocument(); }); });

Cypress Command Reference

Field Interactions

  • fillInputByType(type, value, index?, options?)
  • fillInputByPlaceholder(placeholder, value, options?)
  • fillInputByLabel(label, value, options?)
  • fillTextarea(value, index?, options?)
  • selectDropdownOption(optionValue, dropdownIndex?)
  • selectDropdownByLabel(label, optionValue)
  • checkCheckbox(index?)
  • checkCheckboxByLabel(label)
  • checkSwitch(index?)
  • uncheckCheckbox(index?)
  • uncheckSwitch(index?)
  • moveSlider(value, index?)

Validation

  • expectValidationError(message)
  • expectNoValidationErrors()
  • expectFieldError(fieldLabel, errorMessage)
  • expectFieldValid(fieldLabel)
  • triggerValidation(submitButton?)

Submission

  • submitForm()
  • submitAndExpectSuccess(successIndicator?)
  • submitAndExpectErrors()
  • resetForm()
  • interceptFormSubmission(method, url, alias)

State

  • verifyFormExists()
  • verifyFieldExists(selector)
  • verifyFieldValue(selector, value)
  • verifyFieldCount(selector, count)
  • getFormData()

Complex Flows

  • fillCompleteForm(formData)
  • testFieldInteraction(fieldType, value)
  • testFormFlow(steps)

Convenience

  • fillEmail(value)
  • fillPhone(value)
  • fillPassword(value)
  • fillText(value)

Debug

  • logFormState()
  • waitForFormReady()
  • clearForm()
  • verifyFormValid()
  • screenshotForm(name?)

Testing Checklist

âś… Component Testing

  • Components render correctly
  • Props are passed correctly
  • Event handlers are called
  • Validation errors are displayed
  • Loading states work

âś… Integration Testing

  • Forms submit with valid data
  • Validation errors are shown
  • Error states are handled
  • Success states are shown
  • Dynamic fields work

âś… Accessibility Testing

  • No accessibility violations
  • ARIA attributes are correct
  • Keyboard navigation works
  • Screen reader support
  • Focus management

âś… Performance Testing

  • Render time is within budget
  • Memory usage is reasonable
  • Large forms perform well
  • No memory leaks

Troubleshooting

Common Issues

Element Not Found

Error: Timed out retrying: cy.get() failed because the element was not found

Fix: Use semantic selectors and wait for elements

// âś… Solution cy.fillInputByLabel('Email', 'test@example.com'); // or cy.waitForFormReady();

Validation Errors Not Showing

Fix: Trigger validation explicitly

cy.fillInputByLabel('Email', 'invalid-email'); cy.triggerValidation(); cy.expectValidationError('Invalid email');

Next Steps

Last updated on