I have this component, that is a form for a client regitrastion, it works well on the browser, but when I run my tests, I get an error:
import * as country from "@/entities/country"
import { Button, Input, Form, message, Select, DatePicker, AutoComplete, Upload } from 'antd';
import { toast } from 'react-toastify';
import { useNavigate } from 'react-router-dom';
import { registerCustomerFx, uploadCustomerDocumetFx } from '@/entities/customer';
import { useUnit } from 'effector-react';
import { useEffect, useState } from "react";
import { removeAccents } from "@/shared/utils/word";
import dayjs from "dayjs";
import { RcFile, UploadProps } from "antd/es/upload";
import { FileType, getBase64 } from "@/shared/lib/image";
import { UploadOutlined } from '@ant-design/icons';
import { UploadButton } from "@/shared/ui/UploadButton";
import { africanCountryCodes } from "@/shared/data/ddd";
import { documentTypes } from "@/shared/data/documents";
export const ClientRegistrationForm = () => {
const [ form ] = Form.useForm();
const navigate = useNavigate();
const countriesList = useUnit(country.$getCountriesState);
const [ imageUrl, setImageUrl ] = useState<string>();
const [ loading, setLoading ] = useState(false);
const [ documentType, setDocumentType ] = useState("");
const onFinish = async (values: any) => {
try {
const { prefix, phoneNumber, documentType, clientDocuments, ...registValues } = values;
const response = await registerCustomerFx({
...registValues,
phoneNumber: `${ prefix }${ phoneNumber }`,
documents: [{
title: documentType,
type: documentType,
urls: clientDocuments.map((item: any) => item.response.url)
}]
});
message.success('Documentos submetidos com sucesso.');
navigate(`/clientes/${ response.data.customer._id }/registrar-apolice`);
} catch (error: any) {
toast.error(error.response.data.message);
}
};
const handleUpload = async ({ file, onSuccess, name }: { file: string | Blob | RcFile; onSuccess: Function | undefined; name: string }) => {
try {
const formData = new FormData();
formData.append('file', file);
const response = await uploadCustomerDocumetFx(formData);
const data = response.data;
const { url } = data;
const fieldValue = form.getFieldValue(name);
const updatedFieldValue = [...(fieldValue || []), url];
form.setFieldsValue({ [name]: updatedFieldValue });
if (onSuccess) {
onSuccess(data, file);
}
message.success("Ficheiro carregado!");
} catch (error) {
message.error("Ocorreu algum erro!");
}
};
const handleChange: UploadProps['onChange'] = (info) => {
if (info.file.status === 'uploading') {
setLoading(true);
return;
}
if (info.file.status === 'done') {
getBase64(info.file.originFileObj as FileType, (url) => {
setLoading(false);
setImageUrl(url);
});
}
};
useEffect(() => {
country.getCountriesFx();
}, []);
return (
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{
prefix: "+258"
}}
data-testid="client-registration-form"
>
<h2 className="text-2xl font-semibold mb-5">Registrar novo cliente</h2>
<div className="grid grid-cols-1 md:grid-cols-2 w-full gap-2">
<Form.Item
name="firstName"
label="Primeiro nome"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, coloque o primeiro nome do cliente!' }]}
>
<Input placeholder="John" className="py-3" />
</Form.Item>
<Form.Item
name="lastName"
label="Último nome"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, coloque o último nome do cliente!' }]}
>
<Input placeholder="Doe" className="py-3" />
</Form.Item>
<Form.Item
name="nuit"
label="NUIT"
className="w-full md:max-w-sm"
rules={[{ required: false, message: 'Por favor, coloque o nuit!' }]}
>
<Input placeholder="767867856" className="py-3" />
</Form.Item>
<Form.Item
name="identityCard"
label="Número de BI"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, coloque o número de documento!' }]}
>
<Input placeholder="6786786667M" className="py-3" />
</Form.Item>
<Form.Item
name="country"
label="Nacionalidade"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, selecione um país!' }]}
>
<AutoComplete
size="large"
placeholder="Selecione o país"
options={ countriesList }
filterOption={(inputValue, option) =>
removeAccents(option?.label).toUpperCase().indexOf(removeAccents(inputValue.toUpperCase())) !== -1
}
/>
</Form.Item>
<Form.Item
name="phoneNumber"
label="Número de celular"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, coloque o número de celular!' }]}
>
<Input
addonBefore={
<Form.Item name="prefix" noStyle>
<AutoComplete
size="large"
options={ africanCountryCodes }
style={{ width: 100 }}
filterOption={(inputValue, option) =>
option?.label.indexOf(inputValue.toUpperCase()) !== -1
}
/>
</Form.Item>
}
placeholder="841234567"
/>
</Form.Item>
<Form.Item
name="genre"
label="Genero"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, selecione o genero!' }]}
>
<Select size="large" data-testid="genre" placeholder="Selecione o genero">
<Select.Option data-testid="genre-male" value="masculino">Masculino</Select.Option>
<Select.Option data-testid="genre-female" value="femenino">Femenino</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Data de Nascimento"
name="birthDate"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, insira a data de nascimento' }]}
>
<DatePicker
format="DD/MM/YYYY"
maxDate={dayjs(new Date()).subtract(18, "year")}
className="relative flex gap-3 w-full p-2 py-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10"
/>
</Form.Item>
<Form.Item
name="address"
label="Endereço"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, coloque o endereço!' }]}
>
<Input placeholder="Mavalane A" className=" py-3" />
</Form.Item>
<Form.Item
name="documentType"
label="Tipo de documento"
className="w-full md:max-w-sm"
rules={[{ required: true, message: 'Por favor, selecione o tipo de documento!' }]}
>
<Select
size="large"
placeholder="Selecione o genero"
onChange={ (value) => {
setDocumentType(value);
} }
>
{
documentTypes.map((item) => {
return (
<Select.Option key={item.value} value={item.value}>{ item.label }</Select.Option>
)
})
}
</Select>
</Form.Item>
<Form.Item
name="clientDocuments"
label="Carregue o documento"
valuePropName="fileList"
getValueFromEvent={ e => e && e.fileList }
rules={[{ required: true, message: 'Por favor, coloque os documentos do cliente!' }]}
>
{
documentType === "NUIT" ? (
<Upload
customRequest={({ file, onSuccess }) => handleUpload({ file, onSuccess, name: 'clientDocuments' })}
accept="image/*"
listType="picture-card"
showUploadList={ false }
capture={"environment"}
className="py-3"
onChange={ handleChange }
>
{ imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '100%' }} /> : <UploadButton loading={ loading } /> }
</Upload>
) : (
<Upload
multiple
maxCount={2}
customRequest={({ file, onSuccess }) => handleUpload({ file, onSuccess, name: 'clientDocuments' })}
accept="image/*"
listType="picture"
capture={"environment"}
className="py-3"
>
<Button icon={<UploadOutlined />}>Carregar (frente e verso)</Button>
</Upload>
)
}
</Form.Item>
</div>
<Form.Item>
<Button type="primary" htmlType="submit" className='group relative w-full md:max-w-52 flex py-3 px-4 justify-center border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 h-full'>
Submeter
</Button>
</Form.Item>
</Form>
)
};
And a test written using testing-library and jest that is in the same direcory:
import { fireEvent, render, waitFor } from "@/shared/utils/test-utils";
import { ClientRegistrationForm } from ".";
import * as country from "@/entities/country";
import { registerCustomerFx } from "@/entities/customer";
import dayjs from "dayjs";
import selectEvent from 'react-select-event';
jest.mock("@/entities/customer", () => ({
registerCustomerFx: jest.fn().mockResolvedValueOnce({
data: { customer: { _id: "1" } }
}),
uploadCustomerDocumetFx: jest.fn().mockResolvedValueOnce({
data: {
url: "https://example.com/file.txt",
}
}),
}));
jest.mock("@/entities/country", () => {
const { $getCountriesState } = jest.requireActual("@/entities/country");
return {
getCountriesFx: jest.fn(),
$getCountriesState: {
...$getCountriesState,
getState: jest.fn()
}
}
});
describe("Client registration feature", () => {
beforeAll(() => {
jest.spyOn(country.$getCountriesState, 'getState').mockReturnValue([
{ label: 'Mozambique', value: 'MZ' },
{ label: 'South Africa', value: 'ZA' },
]);
});
it("should render client registration", () => {
const screen = render(<ClientRegistrationForm />);
// Just a test
});
it("should call registerCustomerFx with correct values on form submit", async () => {
const mockRegisterCustomer = registerCustomerFx as unknown as jest.Mock;
const todaysDate = dayjs().format("DD/MM/YYYY");
const screen = render(<ClientRegistrationForm />);
fireEvent.change(screen.getByLabelText(/primeiro nome/i), { target: { value: "Jeffer" } });
fireEvent.change(screen.getByLabelText(/último nome/i), { target: { value: "Sunde" } });
fireEvent.change(screen.getByLabelText(/nuit/i), { target: { value: "123456789" } });
fireEvent.change(screen.getByLabelText(/número de bi/i), { target: { value: "1234567890M" } });
fireEvent.change(screen.getByLabelText(/nacionalidade/i), { target: { value: "MZ" } });
fireEvent.change(screen.getByLabelText(/número de celular/i), { target: { value: "843997730" } });
selectEvent.select(screen.getByLabelText(/genero/i), "masculino");
fireEvent.change(screen.getByLabelText(/data de nascimento/i), { target: { value: todaysDate } });
fireEvent.change(screen.getByLabelText(/endereço/i), { target: { value: "Mavalane" } });
selectEvent.select(screen.getByLabelText(/tipo de documento/i), "NUIT");
const file = new File(['file contents'], 'filename.txt', { type: 'text/plain' });
fireEvent.change(screen.getByLabelText(/carregue o documento/i), { target: { files: [file] } });
fireEvent.submit(screen.getByTestId("client-registration-form"));
await waitFor(() => {
expect(mockRegisterCustomer).toHaveBeenCalledWith({
firstName: "Jeffer",
lastName: "Sunde",
nuit: "123456789",
identityCard: "1234567890M",
country: "MZ",
phoneNumber: "+258843997730",
genre: "masculino",
birthDate: todaysDate,
address: "Mavalane",
documents: [{
title: "BI",
type: "BI",
urls: ["https://example.com/file.txt"]
}]
});
});
});
});
But when I run my tests, I get the following error:
jeffermarcelino@Jeffers-MacBook-Pro imperial-seguros % npx jest src/features/client-registration
FAIL src/features/client-registration/client-registration.test.tsx (13.417 s)
Client registration feature
✓ should render client registration (707 ms)
✕ should call registerCustomerFx with correct values on form submit (2770 ms)
● Client registration feature › should call registerCustomerFx with correct values on form submit
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: {"address": "Mavalane", "birthDate": "19/03/2024", "country": "MZ", "documents": [{"title": "BI", "type": "BI", "urls": ["https://example.com/file.txt"]}], "firstName": "Jeffer", "genre": "masculino", "identityCard": "1234567890M", "lastName": "Sunde", "nuit": "123456789", "phoneNumber": "+258843997730"}
Number of calls: 0
Ignored nodes: comments, script, style
<html>
<head />
<body>
<div>
<form
class="ant-form ant-form-vertical css-dev-only-do-not-override-1xg9z9n"
data-testid="client-registration-form"
>
<h2
class="text-2xl font-semibold mb-5"
>
Registrar novo cliente
</h2>
<div
class="grid grid-cols-1 md:grid-cols-2 w-full gap-2"
>
<div
class="ant-form-item w-full md:max-w-sm css-dev-only-do-not-override-1xg9z9n ant-form-item-has-success"
>
<div
class="ant-row ant-form-item-row css-dev-only-do-not-override-1xg9z9n"
>
<div
class="ant-col ant-form-item-label css-dev-only-do-not-override-1xg9z9n"
>
<label
class="ant-form-item-required"
for="firstName"
title="Primeiro nome"
>
Primeiro nome
</label>
</div>
<div
class="ant-col ant-form-item-control css-dev-only-do-not-override-1xg9z9n"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input css-dev-only-do-not-override-1xg9z9n ant-input-outlined ant-input-status-success py-3"
id="firstName"
placeholder="John"
type="text"
value="Jeffer"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item w-full md:max-w-sm css-dev-only-do-not-override-1xg9z9n ant-form-item-has-success"
>
<div
class="ant-row ant-form-item-row css-dev-only-do-not-override-1xg9z9n"
>
<div
class="ant-col ant-form-item-label css-dev-only-do-not-override-1xg9z9n"
>
<label
class="ant-form-item-required"
for="lastName"
title="Último nome"
>
Último nome
</label>
</div>
<div
class="ant-col ant-form-item-control css-dev-only-do-not-override-1xg9z9n"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
aria-required="true"
class="ant-input css-dev-only-do-not-override-1xg9z9n ant-input-outlined ant-input-status-success py-3"
id="lastName"
placeholder="Doe"
type="text"
value="Sunde"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item w-full md:max-w-sm css-dev-only-do-not-override-1xg9z9n ant-form-item-has-success"
>
<div
class="ant-row ant-form-item-row css-dev-only-do-not-override-1xg9z9n"
>
<div
class="ant-col ant-form-item-label css-dev-only-do-not-override-1xg9z9n"
>
<label
class=""
for="nuit"
title="NUIT"
>
NUIT
</label>
</div>
<div
class="ant-col ant-form-item-control css-dev-only-do-not-override-1xg9z9n"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input css-dev-only-do-not-override-1xg9z9n ant-input-outlined ant-input-status-success py-3"
id="nuit"
placeholder="767867856"
type="text"
value="123456789"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item w-full md:max-w-sm css-dev-only-do-not-override-1xg9z9n ant-form-item-has-success"
>
<div
class="ant-row ant-form-item-row css-dev-only-do-not-override-1xg9z9n"
>
<div
class="ant-col ant-form-item-label css-dev-only-do-not-override-1xg9z9n"
>
<label
class="ant-form-item-required"
for="i...
77 |
78 | await waitFor(() => {
> 79 | expect(mockRegisterCustomer).toHaveBeenCalledWith({
| ^
80 | firstName: "Jeffer",
81 | lastName: "Sunde",
82 | nuit: "123456789",
at src/features/client-registration/client-registration.test.tsx:79:36
at runWithExpensiveErrorDiagnosticsDisabled (node_modules/@testing-library/dom/dist/config.js:47:12)
at checkCallback (node_modules/@testing-library/dom/dist/wait-for.js:124:77)
at checkRealTimersCallback (node_modules/@testing-library/dom/dist/wait-for.js:118:16)
at Timeout.task [as _onTimeout] (node_modules/jsdom/lib/jsdom/browser/Window.js:520:19)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 14.728 s, estimated 55 s
Ran all test suites matching /src\/features\/client-registration/i.
My @/shared/utils/test-utils has the following things:
import { ReactElement } from 'react';
import { render as rtlRender, RenderOptions } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
type CustomRenderOptions = Omit<RenderOptions, 'queries'>;
const customRender = (ui: ReactElement, options?: CustomRenderOptions) =>
rtlRender(ui, { wrapper: BrowserRouter, ...options });
export * from '@testing-library/react';
export { customRender as render };
Can you guys help me solving this?