MockedProvider does not work when Component query changes - reactjs

I have simplified problem into these files
TestComp.tsx
export const TestComp: FC<> = () => {
const [testValue, setTestValue] = React.useState(5);
const where = React.useMemo(() => testValue, [testValue]);
const { data: productsData } = useQuery<QueryProductsResult>(QUERY_PRODUCTS, {
variables: {
where,
},
});
setTimeout(() => {
setTestValue(7);
}, 0);
console.log(productsData, where);
return <div>content</div>;
};
TestComp.test.tsx
const productsData = [
{
... some data ...
},
];
const mockContractPricing = [
{
request: {
query: QUERY_PRODUCTS,
variables: {
where: 7,
},
},
result: {
data: {
products: productsData,
},
},
},
];
describe('dsfgdas', () => {
it('ggg', async () => {
const { container } = render(
<MockedProvider mocks={mockContractPricing} addTypename={false}>
<TestComp />
</MockedProvider>
);
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
console.log(container.innerHTML);
});
});
So when where is updated it does not work. If I put where=7 since in the beginning it does not work. But if it is changing inside the component it never works out. Even trying to make it where: 7 in the mocked query it doesn't work.
The setTimeout is put only for demonstration purpose.
Is there any way to do it?
Console output:
console.log
undefined 5
console.log
undefined 7
console.log
content

Related

How can I test useEffect with async function in Jest?

I have this function inside a helper:
export const useDAMProductImages = (imageId: string) => {
const {
app: { baseImgDomain },
} = getConfig();
const response: MutableRefObject<string[]> = useRef([]);
useEffect(() => {
const getProductImages = async (imageId: string) => {
try {
const url = new URL(FETCH_URL);
const res = await fetchJsonp(url.href, {
jsonpCallbackFunction: 'callback',
});
const jsonData = await res.json();
response.current = jsonData;
} catch (error) {
response.current = ['error'];
}
};
if (imageId) {
getProductImages(imageId);
}
}, [imageId]);
return response.current;
};
In test file:
import .....
jest.mock('fetch-jsonp', () =>
jest.fn().mockImplementation(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve({ set: { a: 'b' } }),
}),
),
);
describe('useDAMProductImages', () => {
beforeEach(() => {
jest.clearAllMocks();
cleanup();
});
it('should return empty array', async () => {
const { result: hook } = renderHook(() => useDAMProductImages('a'), {});
expect(hook.current).toMatchObject({ set: { a: 'b' } });
});
});
The problem is that hook.current is an empty array. Seems that useEffect is never called. Can someone explain to me what I'm doing wrong and how I should write the test? Thank you in advance

update React state after fetching referenced document

I have a simple React App using Firestore.
I have a document in Firestore:
{
date: November 20, 2022 at 11:24:44 AM UTC+1,
description: "...",
title: "Dummy title",
type: "student",
userRef: /users/0AjB4yFFcIS6VMQMi7rUnF3eJXk2
}
Now I have a custom hook, that fetches the data:
export const useAnnouncements = () => {
const [announcements, setAnnouncements] = useState([]);
useEffect(() => {
getAnnouncements().then((documents) => {
const documentsList = [];
documents.forEach((doc) => {
const document = { id: doc.id, ...doc.data() };
getUser(document.userRef).then((u) => {
document.user = u.data(); // <-- HERE is problem
});
documentsList.push(document);
setAnnouncements(documentsList);
});
});
}, []);
return [announcements];
};
Problem is that I have a REFERENCE field type, and it has to be fetched separately. Result? My list is populated, but first without user. Later, when the users' data is fetched, the state is not being updated.
How to deal with React + Firestore's reference field?
Array.prototype.forEach is not designed for asynchronous code. (It was not suitable for promises, and it is not suitable for async-await.) instead you can use map.
useEffect(() => {
getAnnouncements().then((documents) => {
const promises = documents.map((doc) => {
return getUser(doc.userRef).then((u) => {
const document = { id: doc.id, user: u.data(), ...doc.data() };
return document;
});
});
Promise.all(promises).then((documentsList) => {
setAnnouncements(documentsList);
});
});
}, []);
I think you need to wait for all the data to be fetched
export const useAnnouncements = () => {
const [announcements, setAnnouncements] = useState([]);
useEffect(() => {
let isValidScope = true;
const fetchData = async () => {
const documents = await getAnnouncements();
if (!isValidScope) { return; }
const allPromises = documents?.map(doc => {
return getUser(doc.userRef)
.then(user => {
return {
id: doc.id,
...doc.data(),
user: user.data()
}
})
}
const documentsList = await Promise.all(allPromises);
if (!isValidScope) { return; }
setAnnouncements(documentsList);
}
fetchData()
return () => { isValidScope = false }
}, []);
return [announcements];
};
Hope it helps in some way

Testing Optimistic update in react query

I am trying to write the test case for an optimistic update in react query. But it's not working. Here is the code that I wrote to test it. Hope someone could help me. Thanks in advance. When I just write the onSuccess and leave an optimistic update, it works fine but here it's not working. And how can we mock the getQueryData and setQueryData here?
import { act, renderHook } from "#testing-library/react-hooks";
import axios from "axios";
import { createWrapper } from "../../test-utils";
import { useAddColorHook, useFetchColorHook } from "./usePaginationReactQuery";
jest.mock("axios");
describe('Testing custom hooks of react query', () => {
it('Should add a new color', async () => {
axios.post.mockReturnValue({data: [{label: 'Grey', id: 23}]})
const { result, waitFor } = renderHook(() => useAddColorHook(1), { wrapper: createWrapper() });
await act(() => {
result.current.mutate({ label: 'Grey' })
})
await waitFor(() => result.current.isSuccess);
})
})
export const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
}
});
export function createWrapper() {
const testQueryClient = createTestQueryClient();
return ({ children }) => (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
}
export const useAddColorHook = (page) => {
const queryClient = useQueryClient()
return useMutation(addColor, {
// onSuccess: () => {
// queryClient.invalidateQueries(['colors', page])
// }
onMutate: async color => {
// newHero refers to the argument being passed to the mutate function
await queryClient.cancelQueries(['colors', page])
const previousHeroData = queryClient.getQueryData(['colors', page])
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
return { previousHeroData }
},
onSuccess: (response, variables, context) => {
queryClient.setQueryData(['colors', page], (oldQueryData) => {
console.log(oldQueryData, 'oldQueryData', response, 'response', variables, 'var', context, 'context', 7984)
return {
...oldQueryData,
data: oldQueryData.data.map(data => data.label === variables.label ? response.data : data)
}
})
},
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(['colors', page], context.previousHeroData)
},
onSettled: () => {
queryClient.invalidateQueries(['colors', page])
}
})
}
The error that you are getting actually shows a bug in the way you've implemented the optimistic update:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
what if there is no entry in the query cache that matches this query key? oldQueryData will be undefined, but you're not guarding against that, you are spreading ...oldQueryData.data and this will error out at runtime.
This is what happens in your test because you start with a fresh query cache for every test.
An easy way out would be, since you have previousHeroData already:
const previousHeroData = queryClient.getQueryData(['colors', page])
if (previousHeroData) {
queryClient.setQueryData(['colors', page], {
...previousHeroData,
data: [...previousHeroData.data, { id: previousHeroData.data.length + 1, ...color }]
}
}
If you are using TanStack/query v4, you can also return undefined from the updater function. This doesn't work in v3 though:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return oldQueryData ? {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
} : undefined
})
This doesn't perform an optimistic update then though. If you know how to create a valid cache entry from undefined previous data, you can of course also do that.

React testing library fimding multiple nodes in the DOM

I am writing tests for my component using react testing library and I am using nested describe blocks for separating tests
const dummyData1 = Array.from({ length: 10 }).map((obj, i) => ({title: "Test" + i,
value: "Test" + i,
key: "Test" + i,
}));
describe("Custom Select", () => {
describe("Single", () => {
afterAll(() => {
cleanup();
});
const loadMore = jest.fn();
const onChange = jest.fn();
loadMore.mockResolvedValueOnce(dummyData2);
const { queryByTestId, getByText, getAllByText, container } = render(
<MultiSelect
data={[{ title: "Test", key: "Test", value: "Test" }, ...dummyData1]}
value="Test"
loadMoreData={loadMore}
onChange={onChange}
/>
);
const selectWrapper = queryByTestId("multi-select-wrapper");
it("Renders successfully -- Single", () => {
expect(selectWrapper).toBeTruthy();
});
it("Value rendered -- Single", () => {
expect(getByText("Test")).toBeInTheDocument();
});
it("Options rendered -- Single", async () => {
act(() => {
fireEvent.click(selectWrapper);
});
const elements = getAllByText("Test");
expect(elements).toHaveLength(1);
});
it("Load More called -- Single", async () => {
act(() => {
fireEvent.click(selectWrapper);
});
const menuEl = container.querySelector(".multi-select-menu");
act(() => {
fireEvent.scroll(menuEl, { target: { scrollY: 700 } });
});
await waitFor(
() => {
expect(loadMore).toBeCalled();
},
{ timeout: 500 }
);
});
it("onChange called -- Single", async () => {
act(() => {
fireEvent.click(selectWrapper);
});
const menuItem = getByText("Test0");
act(() => {
fireEvent.click(menuItem);
});
await waitFor(
() => {
expect(onChange).toHaveBeenCalledWith("Test0");
},
{ timeout: 500 }
);
});
it("Load More called on search -- Single", async () => {
const input = container.querySelector(".multi-select-input");
act(() => {
fireEvent.change(input, { target: { value: "TT" } });
});
await waitFor(
() => {
expect(loadMore).toBeCalledTimes(1);
},
{ timeout: 500 }
);
});
});
describe("Multiple", () => {
beforeAll(() => {
cleanup();
});
const loadMore = jest.fn();
const onChange = jest.fn();
loadMore.mockResolvedValueOnce(dummyData2);
const { queryByTestId, getByText, getAllByText, container } = render(
<MultiSelect
data={[{ title: "Test", key: "Test", value: "Test" }, ...dummyData1]}
value={["Test"]}
loadMoreData={loadMore}
onChange={onChange}
multiple
showCheckboxes
/>
);
const selectWrapper = queryByTestId("multi-select-wrapper");
it("Renders successfully", () => {
expect(selectWrapper).toBeTruthy();
});
it("Value rendered", () => {
expect(getByText("Test")).toBeInTheDocument();
});
});
});
I am getting this error on running the tests. The tests run perfectly if I use them separately. I dont want to render in each test individually but the nested describe is not working
TestingLibraryElementError: Found multiple elements by: [data-testid="multi-select-wrapper"]

how to call useMutation hook from a function?

I have this functional React component:
// CreateNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
const createNotification = (notification) => {
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
};
export default createNotification;
I call the createNotification component in a function and pass in some variables after a other useMutation hook has been called:
// AddMovie.tsx
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
createNotification({movie: movie, user: currentUserVar()});
});
};
When I run the code I get the (obvious) error:
Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component
Because I call the createNotification hook in the addMovie function.
If I move the createNotification to the top of the component:
// AddMovie.tsx
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
createNotification({movie: movie, user: currentUserVar()});
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
};
}
The code works fine, except that the hook is now called every time the AddMovie component is rendered instead of when the addMovie function is called from the click:
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
Figured it out:
// createNotification.tsx
import {useMutation} from '#apollo/client';
import resolvers from '../resolvers';
export const createNotification = () => {
const [createNotification, {data, loading, error}] = useMutation(resolvers.mutations.CreateNotification);
const handleCreateNotification = async (notification) => {
createNotification({
variables: {
movie_id: notification.movie.id,
actor_id: notification.user.id,
message:
`${notification.user.user_name} has added ${notification.movie.original_title} to their watchlist.`,
},
});
console.log(data, loading, error);
};
return {
createNotification: handleCreateNotification,
};
};
If I'm correct then this returns a reference (createNotification) to the function handleCreateNotification
Then in the component I want to use the createNotification helper I import it:
// AddMovie.tsx
import {createNotification} from '../../../../helpers/createNotification';
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const x = createNotification();
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
}).then( async () => {
x.createNotification({movie: movie, user: currentUserVar()});
});
}
};
You (kind of) answer your own question by showing the error and saying it's obvious. createNotification is not a React component, and it is not a custom hook, it is just a function. Thus using a hook inside of it breaks the Rules of Hooks.
If you want to keep that logic in it's own function, that's fine, just redefine your component like this:
const AddMovieToWatchList = ({movie}: {movie: IMovie}) => {
const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie);
const [createNotification] = useMutation(resolvers.mutations.CreateNotification);
const addMovie = async (movie: IMovie) => {
await addUserToMovie({
variables: {...movie, tmdb_id: movie.id},
update: (cache, {data}) => {
cache.modify({
fields: {
moviesFromUser: () => {
return [...data.addUserToMovie];
},
},
});
},
});
await createNotification({movie: movie, user: currentUserVar()});
};
return (
<a className={classes.addMovie} onClick={() => addMovie(movie)}>
Add movie to your watchlist
</a>
);
}

Resources