refinedev / refine

A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.
https://refine.dev
MIT License
28.62k stars 2.24k forks source link

[BUG] Can't test a component that uses useCustomMutation #3432

Closed MetaMmodern closed 1 year ago

MetaMmodern commented 1 year ago

Describe the bug

Components that are using refine hooks are impossible to test.

One of the examples is a component making a custom request to server.

Here is my component code. ```tsx import { FileUpload, UploadOutlined } from '@mui/icons-material'; import { useApiUrl, useCustomMutation } from '@pankod/refine-core'; import { Button, Card, CardActions, CardContent, CardMedia, LoadingButton, Modal, SxProps, Typography, } from '@pankod/refine-mui'; import { FC, useState } from 'react'; import { ThumbnailUploadResponse } from 'types'; const cardStyle = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', maxWidth: 400, }; type ThumbnailUploaderProps = { handleThumbnailUploadSuccess: (link: string) => any; sx?: SxProps; }; const ThumbnailUploader: FC = ({ handleThumbnailUploadSuccess, sx, }) => { const [modalOpen, setModalOpen] = useState(false); const [fileValue, setFileValue] = useState(null); const [fileUrl, setFileUrl] = useState(''); const { mutate, isLoading } = useCustomMutation(); const API_URL = useApiUrl(); const handleOpen = () => setModalOpen(true); const handleClose = () => setModalOpen(false); const handleInputChange: React.ChangeEventHandler = ( event ) => { if (!event.target.files?.length) return; const file = event.target.files[0]; setFileValue(file); setFileUrl(URL.createObjectURL(file)); handleOpen(); event.target.value = ''; }; const handleCancelUpload = () => { URL.revokeObjectURL(fileUrl); setFileUrl(''); handleClose(); }; const handleSubmitUpload = () => { if (!fileValue) { return; } const formData = new FormData(); formData.append('file', fileValue); mutate( { url: `${API_URL}/thumbnail`, values: formData, method: 'post', successNotification: { message: 'Succesfully uploaded thumbnail.', type: 'success', }, errorNotification: { message: 'Could not save thumbnail. Please try again later.', type: 'error', }, }, { onSuccess: (data) => { handleCancelUpload(); handleThumbnailUploadSuccess(data.data.url); }, } ); }; return ( <> Are you sure you want to upload this image as a thumbnail? } loading={isLoading} loadingPosition="start" onClick={handleSubmitUpload} > Upload ); }; export default ThumbnailUploader; ```
Here is my test code. ```tsx /** * @jest-environment jsdom */ import { fireEvent, render, waitFor } from '@testing-library/react'; import axios from 'axios'; import ThumbnailUploader from 'components/ThumbnailUploader'; import { QueryClient, QueryClientProvider } from 'react-query'; import { ThumbnailUploadResponse } from 'types'; const queryClient = new QueryClient(); jest.mock('axios'); const mockedAxios = axios as jest.Mocked; it('', async () => { const url = 'mockedUrlValue'; const resp: ThumbnailUploadResponse = { url }; mockedAxios.post.mockResolvedValue(resp); const file = new File(['(āŒā–”_ā–”)'], 'chucknorris.png', { type: 'image/png', }); const handleThumbnailUploadSuccess = jest.fn((link: string) => link); const { queryByLabelText, getByLabelText, getByTestId } = render( ); const uploadInput = getByTestId('image-input'); await waitFor(() => fireEvent.change(uploadInput, { target: { files: [file] }, }) ); let uploadInputFromDom = document.getElementById( 'image-input' ) as HTMLInputElement | null; // file should be saved to state and cleared from DOM expect(uploadInputFromDom?.files?.length).toBe(0); }); ```

As you can see I even tried wrapping it onto QueryProvider(as suggested here) but it didn't work saying:

No QueryClient set, use QueryClientProvider to set one Screenshot below.

Steps To Reproduce

Just try to use custom hook in separate component and try to run tests for it.

Expected behavior

Should be easy to test components(Or at least documentation should have more info about how to actually unit test components).

Screenshot

image

Desktop

-

Mobile

Additional Context

No response

aliemir commented 1 year ago

Hey @MetaMmodern, instead of just wrapping the component to QueryClientProvider, you probably need to prepare a test wrapper with <Refine /> component. Let me share you our UI package tests and wrappers that might give you an idea about that,

@pankod/refine-antd's test setup here: https://github.com/refinedev/refine/blob/next/packages/antd/test/index.tsx#L28 @pankod/refine-mantine's here: https://github.com/refinedev/refine/blob/next/packages/mantine/test/index.tsx

ErrorComponent's test here as an example of component testing https://github.com/refinedev/refine/blob/next/packages/antd/src/components/pages/error/index.spec.tsx#L21-L27

Edit component's test here for testing with routing https://github.com/refinedev/refine/blob/next/packages/antd/src/components/crud/edit/index.spec.tsx#L10-L55

useTable hook test example here: https://github.com/refinedev/refine/blob/next/packages/antd/src/hooks/table/useTable/useTable.spec.ts#L27-L29

You can check out our codebase for more details.

Hope this helps!

MetaMmodern commented 1 year ago

@aliemir Thank you! I'm using mui so I guess this one will help me: https://github.com/refinedev/refine/blob/next/packages/mui/test/index.tsx

Please don't close this issue, I'll see how this works and get back to you(hopefully today)

MetaMmodern commented 1 year ago

Ok, I see, I need to create my own wrapper. It would be nice though if you could expose those wrappers, they might bbe useful for most cases.

MetaMmodern commented 1 year ago

@aliemir awesome, works perfectly) I have a side question maybe you may know. As you saw above I've mocked axios post call and currently I'm trying to check what was that mock called with(with what arguments) and I see that it's being called with undefined. Do you maybe know what may be the reason for that?

I've updated the mock block so it's now like this:

  const url = 'mockedUrlValue';
  const resp: { data: ThumbnailUploadResponse } = { data: { url } };
  mockedAxios.post.mockResolvedValue(resp);

So in tests I see it's being called after mutate being called, but value is undefined:

image

UPDATE: okay, somehow I made it work and I see that it's upper-layer mock kicks in:

image

I guess the case is solved, I have to provide my dataProvider here instead of default one and it'll work.

MetaMmodern commented 1 year ago

@aliemir what if I get this error when trying to provide a custom daatProvider: There is no "default" data provider. Please pass dataProviderName.

image image

UPDATE: I found the reason. I had to add all methods(or some of required ones) to dataProvider so it counts as a normal dataProvider. Again, case solved) hehe

aliemir commented 1 year ago

Hey @MetaMmodern sorry missed your comments but I guess the case resolved, right? šŸ˜…

aliemir commented 1 year ago

Ok, I see, I need to create my own wrapper. It would be nice though if you could expose those wrappers, they might bbe useful for most cases.

Not all of the props are required and we thought exporting those mocks might lead to misunderstandings since they're just dummy placeholders and we need to mock properly when we need them in tests šŸ˜…

MetaMmodern commented 1 year ago

Hey @MetaMmodern sorry missed your comments but I guess the case resolved, right? šŸ˜…

Yes, case resolved, thanks a lot) you can close the ticket if you want

MetaMmodern commented 1 year ago

Ok, I see, I need to create my own wrapper. It would be nice though if you could expose those wrappers, they might bbe useful for most cases.

Not all of the props are required and we thought exporting those mocks might lead to misunderstandings since they're just dummy placeholders and we need to mock properly when we need them in tests šŸ˜…

Well, according to code most of props of TestWrapper are optional. And that's pretty comfy for user tests, because in my case I just copypasted your testwrapper and used it as is. Only on test I provided my own dataProvider, I didn't have ro pass all the other providers since they were mocked by TestWrapper if props not passed.

omeraplak commented 1 year ago

everything seems to be solved about this issue :)

kimsean commented 1 year ago

@aliemir what if I get this error when trying to provide a custom daatProvider: There is no "default" data provider. Please pass dataProviderName. image image

UPDATE: I found the reason. I had to add all methods(or some of required ones) to dataProvider so it counts as a normal dataProvider. Again, case solved) hehe

Hi. May i know your where did you get RefineTestWrapper ?