import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import { TravelEnquiryForm } from '../index'; import * as submitModule from '../submitEnquiry'; import * as utils from '../utils'; jest.mock('../submitEnquiry'); const mockedSubmit = submitModule.submitEnquiry as jest.MockedFunction< typeof submitModule.submitEnquiry >; describe('TravelEnquiryForm', () => { beforeEach(() => { jest.resetAllMocks(); }); it('should render the form with title', () => { const { getByText } = render(); expect(getByText('Enquiry form')).toBeInTheDocument(); }); it('should render subtitle', () => { const { getByText } = render(); expect( getByText( /Fill in the form below to describe what you want from your next/ ) ).toBeInTheDocument(); }); it('should start on step 1', () => { const { getByLabelText } = render(); expect(getByLabelText(/First Name/)).toBeInTheDocument(); }); it('should render the step indicator', () => { const { getAllByText } = render(); // "Your details" appears in both step indicator and section header expect(getAllByText('Your details').length).toBeGreaterThanOrEqual(1); }); it('should call onClose when overlay is clicked', () => { const onClose = jest.fn(); const { getByTestId } = render(); // Clicking the form container should NOT close (stopPropagation) fireEvent.click(getByTestId('travel-enquiry-form')); expect(onClose).not.toHaveBeenCalled(); }); it('should call onClose when X button is clicked', () => { const onClose = jest.fn(); const { getByLabelText } = render(); fireEvent.click(getByLabelText('Close form')); expect(onClose).toHaveBeenCalledTimes(1); }); describe('step navigation', () => { it('should show validation errors when clicking Next with empty fields', () => { const { getByText } = render(); fireEvent.click(getByText('Next')); expect(getByText('Please enter your first name')).toBeInTheDocument(); expect(getByText('Please enter your last name')).toBeInTheDocument(); }); it('should advance to step 2 when step 1 is valid', () => { const { getByText, getByLabelText } = render(); fireEvent.change(getByLabelText(/First Name/), { target: { value: 'John' } }); fireEvent.change(getByLabelText(/Last Name/), { target: { value: 'Doe' } }); fireEvent.change(getByLabelText(/Telephone Number/), { target: { value: '07700900000' } }); fireEvent.change(getByLabelText(/Email Address/), { target: { value: 'john@example.com' } }); fireEvent.click(getByText('Next')); expect( getByLabelText(/Which port\/airport would you like to go from/) ).toBeInTheDocument(); }); it('should go back to step 1 from step 2', () => { const { getByText, getByLabelText } = render(); // Fill step 1 fireEvent.change(getByLabelText(/First Name/), { target: { value: 'John' } }); fireEvent.change(getByLabelText(/Last Name/), { target: { value: 'Doe' } }); fireEvent.change(getByLabelText(/Telephone Number/), { target: { value: '07700900000' } }); fireEvent.change(getByLabelText(/Email Address/), { target: { value: 'john@example.com' } }); fireEvent.click(getByText('Next')); fireEvent.click(getByText('Back')); expect(getByLabelText(/First Name/)).toBeInTheDocument(); }); it('should not show Back button on step 1', () => { const { queryByText } = render(); expect(queryByText('Back')).not.toBeInTheDocument(); }); it('should show Back button on step 2', () => { const { getByText, getByLabelText } = render(); // Fill step 1 and advance fireEvent.change(getByLabelText(/First Name/), { target: { value: 'John' } }); fireEvent.change(getByLabelText(/Last Name/), { target: { value: 'Doe' } }); fireEvent.change(getByLabelText(/Telephone Number/), { target: { value: '07700900000' } }); fireEvent.change(getByLabelText(/Email Address/), { target: { value: 'john@example.com' } }); fireEvent.click(getByText('Next')); expect(getByText('Back')).toBeInTheDocument(); }); }); describe('form submission', () => { const fillAndNavigateToStep3 = ( getByText: any, getByLabelText: any, getByRole: any ) => { // Step 1 fireEvent.change(getByLabelText(/First Name/), { target: { value: 'John' } }); fireEvent.change(getByLabelText(/Last Name/), { target: { value: 'Doe' } }); fireEvent.change(getByLabelText(/Telephone Number/), { target: { value: '07700900000' } }); fireEvent.change(getByLabelText(/Email Address/), { target: { value: 'john@example.com' } }); fireEvent.click(getByText('Next')); // Step 2 — text inputs fireEvent.change( getByLabelText(/Which port\/airport would you like to go from/), { target: { value: 'Heathrow' } } ); fireEvent.change(getByLabelText(/Where would you like to go/), { target: { value: 'Maldives' } }); // Month CustomSelect: open then click option fireEvent.click(document.getElementById('eq-month') as HTMLElement); fireEvent.click(getByRole('option', { name: 'January' })); // Year CustomSelect fireEvent.click(document.getElementById('eq-year') as HTMLElement); fireEvent.click(getByRole('option', { name: '2026' })); // Duration CustomSelect fireEvent.click(document.getElementById('eq-duration') as HTMLElement); fireEvent.click(getByRole('option', { name: '11 – 14' })); // Adults CustomSelect fireEvent.click(document.getElementById('eq-adults') as HTMLElement); fireEvent.click(getByRole('option', { name: '2' })); // Children CustomSelect fireEvent.click(document.getElementById('eq-children') as HTMLElement); fireEvent.click(getByRole('option', { name: '0' })); fireEvent.click(getByText('Next')); }; it('should show Submit button on step 3', () => { const { getByText, getByLabelText, getByRole } = render( ); fillAndNavigateToStep3(getByText, getByLabelText, getByRole); expect(getByText('Submit enquiry')).toBeInTheDocument(); }); it('should call submitEnquiry on submit', async () => { mockedSubmit.mockResolvedValue(); const { getByText, getByLabelText, getByRole } = render( ); fillAndNavigateToStep3(getByText, getByLabelText, getByRole); fireEvent.click(getByText('Submit enquiry')); await waitFor(() => { expect(mockedSubmit).toHaveBeenCalledTimes(1); }); }); it('should show success screen after successful submission', async () => { mockedSubmit.mockResolvedValue(); const { getByText, getByLabelText, getByRole } = render( ); fillAndNavigateToStep3(getByText, getByLabelText, getByRole); fireEvent.click(getByText('Submit enquiry')); await waitFor(() => { expect(getByText('Enquiry submitted!')).toBeInTheDocument(); }); }); it('should show error message on submission failure', async () => { mockedSubmit.mockRejectedValue(new Error('Server error')); const trackSpy = jest.spyOn(utils, 'trackEnquiryEvent'); const { getByText, getByLabelText, getByRole } = render( ); fillAndNavigateToStep3(getByText, getByLabelText, getByRole); fireEvent.click(getByText('Submit enquiry')); await waitFor(() => { expect( getByText(/Something went wrong when submitting your enquiry/) ).toBeInTheDocument(); }); expect(trackSpy).toHaveBeenCalledWith( 'enquiry form : step 3 : error message displayed', 'automated', 'Something went wrong when submitting your enquiry' ); trackSpy.mockRestore(); }); }); describe('error clearing', () => { it('should clear field error when user starts typing', () => { const { getByText, getByLabelText, queryByText } = render( ); // Trigger validation fireEvent.click(getByText('Next')); expect(getByText('Please enter your first name')).toBeInTheDocument(); // Start typing in the field fireEvent.change(getByLabelText(/First Name/), { target: { value: 'J' } }); expect( queryByText('Please enter your first name') ).not.toBeInTheDocument(); }); }); describe('scrollToTop', () => { it('should call scrollTo when navigating to a new step', () => { const scrollToMock = jest.fn(); HTMLDivElement.prototype.scrollTo = scrollToMock; const { getByText, getByLabelText } = render(); fireEvent.change(getByLabelText(/First Name/), { target: { value: 'John' } }); fireEvent.change(getByLabelText(/Last Name/), { target: { value: 'Doe' } }); fireEvent.change(getByLabelText(/Telephone Number/), { target: { value: '07700900000' } }); fireEvent.change(getByLabelText(/Email Address/), { target: { value: 'john@example.com' } }); fireEvent.click(getByText('Next')); expect(scrollToMock).toHaveBeenCalledWith(0, 0); delete (HTMLDivElement.prototype as any).scrollTo; }); }); describe('handleSubmit validation branch', () => { it('should track error events and return early when step 3 validation fails', async () => { const { getByText, getByLabelText, getByRole } = render( ); // Navigate to step 3 using real validation fireEvent.change(getByLabelText(/First Name/), { target: { value: 'John' } }); fireEvent.change(getByLabelText(/Last Name/), { target: { value: 'Doe' } }); fireEvent.change(getByLabelText(/Telephone Number/), { target: { value: '07700900000' } }); fireEvent.change(getByLabelText(/Email Address/), { target: { value: 'john@example.com' } }); fireEvent.click(getByText('Next')); fireEvent.change( getByLabelText(/Which port\/airport would you like to go from/), { target: { value: 'Heathrow' } } ); fireEvent.change(getByLabelText(/Where would you like to go/), { target: { value: 'Maldives' } }); fireEvent.click(document.getElementById('eq-month') as HTMLElement); fireEvent.click(getByRole('option', { name: 'January' })); fireEvent.click(document.getElementById('eq-year') as HTMLElement); fireEvent.click(getByRole('option', { name: '2026' })); fireEvent.click(document.getElementById('eq-duration') as HTMLElement); fireEvent.click(getByRole('option', { name: '11 – 14' })); fireEvent.click(document.getElementById('eq-adults') as HTMLElement); fireEvent.click(getByRole('option', { name: '2' })); fireEvent.click(document.getElementById('eq-children') as HTMLElement); fireEvent.click(getByRole('option', { name: '0' })); fireEvent.click(getByText('Next')); // On step 3, force validation to return errors so the early-return branch is hit jest.spyOn(utils, 'validateStep').mockReturnValueOnce({ isValid: false, errors: { someField: 'Validation error on step 3' } }); fireEvent.click(getByText('Submit enquiry')); await waitFor(() => { expect(mockedSubmit).not.toHaveBeenCalled(); }); jest.restoreAllMocks(); }); }); });