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/reactAdd 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
- Accessibility Guide - Learn about accessibility features
- Performance Guide - Optimize form performance
- API Reference - Explore testing utilities API
Last updated on