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();
});
});
});