Testing InfiniteScroll component from react-infinite-scroller - reactjs

I'm using react-infinite-scroller to make several calls to a paginated endpoint.
I haven't found any way to test the function that is triggered when a new request has to be done to the server.
I've taken a look at the github repository, but there is no information about how to trigger the function loadMore from a test.
The actual code looks like this:
<PaginatedList
data-testid="paginated-list-testid"
>
<InfiniteScroll
pageStart={0}
loadMore={this._nextPage}
hasMore={hasMore}
loader={<div key={0}>Loading...</div>}
useWindow={false}
>
{this._renderElements()}
</InfiniteScroll>
</PaginatedList>
The _nextPage function looks like follow:
async _nextPage() {
const { hasMore} = this.state;
const { externalCondition } = this.props;
if (hasMore) {
const response = externalCondition ? await this.repositoryA.next() : await this.repositoryB.next();
const nextElements = response.elements;
this.setState({
elements: nextElements,
loading: false,
hasMore: response.hasNext,
});
}
}
I wanna test that given an externalCondition the function this.repositoryA.next() is called. However, my test does not even reach this method.
This is my test:
test('Should\'ve called the repositoryA.next()', async () => {
const sampleUserId = 1;
const paginatedListTestId = "paginated-list-testid";
const renderComponent = render(
<PaginatedList
userId={sampleUserId}
/>,
);
const { getByTestId } = renderComponent;
fireEvent.scroll(getByTestId(paginatedListTestId), { target: { scrollY: 9000 } });
});
I know that my test does not have an assert yet, but I was debugging and I wasn't able to reach my function.
Neither scrolling to 9000 nor -9000.
Any help is welcome.
(The code has been a little bit changed due to privacy, so please, obviate any typo or mismatching variable names)

Related

H5P Instance is duplicated in reactjs

I'm developing with h5p standalone plugin in react (nextjs), passing the path as prop to a Modal Component which render the h5p activity.
useEffect(() => {
const initH5p = async (contentLocation) => {
const { H5P: H5PStandalone } = require('h5p-standalone')
const h5pPath = `https://cdn.thinkeyschool.com/h5p/${contentLocation}`
const options = {
id: 'THINKeyLesson',
h5pJsonPath: h5pPath,
frameJs: '/h5p/dist/frame.bundle.js',
frameCss: '/h5p/dist/styles/h5p.css',
}
let element = document.getElementById('h5p_container')
removeAllChildNodes(element)
await new H5PStandalone(element, options)
fireCompleteH5PTopic(H5P)
setIsLoaderVisible(false)
}
initH5p(location)
}, [location, session.data.user.id, course.slug, topic])
With that code, I get two h5p rendered in screen. So I'm using removeAllChildren() to eliminate them from the render.
function removeAllChildNodes(parent) {
console.log(parent)
while (parent.firstChild) {
parent.removeChild(parent.firstChild)
}
}
That hack is working fine, but when I try to send the xAPI statement to my database, it fires twice
const fireCompleteH5PTopic = async (H5P) => {
H5P.externalDispatcher.on("xAPI", (event) => {
// console.log('event fired')
if (event?.data?.statement?.result?.completion) {
setCounter(counter + 1)
completeH5PTopic(event, session.data.user.id, course.slug, topic)
return true
}
})
}
Any help regarding why it fires twice? I think it may be related to h5p rendering twice too.
Thanks in advance.
I tried using a state to render only once, but it is not working.

React Testing Library Unit Test Case: Unable to find node on an unmounted component

I'm having issue with React Unit test cases.
React: v18.2
Node v18.8
Created custom function to render component with ReactIntl. If we use custom component in same file in two different test cases, the second test is failing with below error.
Unable to find node on an unmounted component.
at findCurrentFiberUsingSlowPath (node_modules/react-dom/cjs/react-dom.development.js:4552:13)
at findCurrentHostFiber (node_modules/react-dom/cjs/react-dom.development.js:4703:23)
at findHostInstanceWithWarning (node_modules/react-dom/cjs/react-dom.development.js:28745:21)
at Object.findDOMNode (node_modules/react-dom/cjs/react-dom.development.js:29645:12)
at Transition.performEnter (node_modules/react-transition-group/cjs/Transition.js:280:71)
at node_modules/react-transition-group/cjs/Transition.js:259:27
If I run in different files or test case with setTimeout it is working as expected and there is no error. Please find the other configs below. It is failing even it is same test case.
setUpIntlConfig();
beforeAll(() => server.listen());
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
jest.clearAllMocks();
server.close();
cleanup();
});
Intl Config:
export const setUpIntlConfig = () => {
if (global.Intl) {
Intl.NumberFormat = IntlPolyfill.NumberFormat;
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
} else {
global.Intl = IntlPolyfill;
}
};
export const RenderWithReactIntl = (component: any) => {
return {
...render(
<IntlProvider locale="en" messages={en}>
{component}
</IntlProvider>
)
};
};
I'm using msw as mock server. Please guide us, if we are missing any configs.
Test cases:
test('fire get resource details with data', async () => {
jest.spyOn(SGWidgets, 'getAuthorizationHeader').mockReturnValue('test-access-token');
process.env = Object.assign(process.env, { REACT_APP_DIAM_API_ENDPOINT: '' });
RenderWithReactIntl(<AllocatedAccess diamUserId={diamUserIdWithData} />);
await waitForElementToBeRemoved(() => screen.getByText(/loading data.../i));
const viewResource = screen.getAllByText(/view resource/i);
fireEvent.click(viewResource[0]);
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
const ownerName = screen.getByText(/benedicte masson/i);
expect(ownerName).toBeInTheDocument();
});
test('fire get resource details with data----2', async () => {
jest.spyOn(SGWidgets, 'getAuthorizationHeader').mockReturnValue('test-access-token');
process.env = Object.assign(process.env, { REACT_APP_DIAM_API_ENDPOINT: '' });
RenderWithReactIntl(<AllocatedAccess diamUserId={diamUserIdWithData} />);
await waitForElementToBeRemoved(() => screen.getByText(/loading data.../i));
const viewResource = screen.getAllByText(/view resource/i);
fireEvent.click(viewResource[0]);
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
const ownerName = screen.getByText(/benedicte masson/i);
expect(ownerName).toBeInTheDocument();
});
Can you try these changes:
test('fire get resource details with data----2', async () => {
jest.spyOn(SGWidgets, 'getAuthorizationHeader').mockReturnValue('test-access-token');
process.env = Object.assign(process.env, { REACT_APP_DIAM_API_ENDPOINT: '' });
RenderWithReactIntl(<AllocatedAccess diamUserId={diamUserIdWithData} />);
await waitFor(() => expect(screen.getByText(/loading data.../i)).not.toBeInTheDocument());
const viewResource = screen.getAllByText(/view resource/i);
act(() => {
fireEvent.click(viewResource[0]);
});
await waitFor(() => expect(screen.getByText(/loading/i)).not.toBeInTheDocument());
expect(screen.getByText(/benedicte masson/i)).toBeVisible();
});
I've got into the habit of using act() when altering something that's visible on the screen. A good guide to here: https://testing-library.com/docs/guide-disappearance/
Using getBy* in the waitFor() blocks as above though, you may be better off specifically checking the text's non-existence.
Without seeing your code it's difficult to go any further. I always say keep tests short and simple, we're testing one thing. The more complex they get the more changes for unforeseen errors. It looks like you're rendering, awaiting a modal closure, then a click, then another modal closure, then more text on the screen. I'd split it into two or more tests.

ReactJS context-api won't render after I get data

This is a next.js site, since both my Navbar component and my cart page should have access to my cart's content I created a context for them. If I try to render the page, I get:
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'key')
obs: The cartContent array exists and has length 1, I can get it by delaying when the data's rendered by using setTimeout, but, can't get it to render right after it's fetched.
I need to make it render after the data from firebase is returned, but always met with the mentioned error.
This is my _app.tsx file
function MyApp({ Component, pageProps }) {
// set user for context
const userContext = startContext();
return (
<UserContext.Provider value = { userContext }>
<Navbar />
<Component {...pageProps} />
<Toaster />
</UserContext.Provider>
);
}
export default MyApp
This file has the startContext function that returns the context so it can be used.
export const startContext = () => {
const [user] = useAuthState(auth);
const [cart, setCart] = useState(null);
const [cartContent, setCartContent] = useState(null);
useEffect(() => {
if (!user) {
setCart(null);
setCartContent(null);
}
else {
getCart(user, setCart, setCartContent);
}
}, [user]);
return { user, cart, setCart, cartContent, setCartContent };
}
This file contains the getCart function.
export const getCart = async (user, setCart, setCartContent) => {
if (user) {
try {
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
if (new_cart) {
let new_cartContent = []
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
console.log(new_cartContent);
setCartContent(new_cartContent);
console.log(new_cartContent);
setCart(new_cart);
}
else {
setCart(null);
setCartContent(null);
}
} catch (err) {
console.error(err);
}
}
}
This is the cart.tsx webpage. When I load it I get the mentioned error.
export default () => {
const { user, cart, cartContent } = useContext(UserContext);
return (
<AuthCheck>
<div className="grid grid-cols-1 gap-4">
{cartContent && cartContent[0].key}
</div>
</AuthCheck>
)
}
I've tried to render the cart's content[0].key in many different ways, but couldn't do it. Always get error as if it were undefined. Doing a setTimeout hack works, but, I really wanted to solve this in a decent manner so it's at least error proof in the sense of not depending on firebase's response time/internet latency.
Edit:
Since it works with setTimeout, it feels like a race condition where if setCartContent is used, it triggers the rerender but setCartContent can't finish before stuff is rendered so it will consider the state cartContent as undefined and won't trigger again later.
Try changing
{cartContent && cartContent[0].key}
to
{cartContent?.length > 0 && cartContent[0].key}
Edit:: The actual problem is in getCart function in line
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
This is either set to an empty array or an empty object. So try changing your if (new_cart) condition to
if (Object.keys(new_cart).length > 0) {
Now you wont get the undefined error
Since there seemed to be a race condition, I figured the setCartContent was executing before its content was fetched. So I changed in the getCart function the map loop with an async function for a for loop
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
to
for (const key of Object.keys(new_cart)) {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
}
I can't make a map function with await in it without making it asynchronous so I the for loop made it work. Hope someone finds some alternatives to solving this, I could only come up with a for loop so the code is synchronous.

Test useRef onError Fn, with React-Testing-Library and Jest

I have this simple fallbackImage Component:
export interface ImageProps {
srcImage: string;
classNames?: string;
fallbackImage?: FallbackImages;
}
const Image = ({
srcImage,
classNames,
fallbackImage = FallbackImages.FALLBACK
}: ImageProps) => {
const imgToSourceFrom = srcImage;
const imgToFallbackTo = fallbackImage;
const imageRef = useRef(null);
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {};
};
return (
<img ref={imageRef} src={imgToSourceFrom} className={classNames} onError={whenImageIsMissing} />
);
};
export default Image;
It works perfectly. I have test running for it with Jest and React-Testing-Library. I have tested all but one scenario. This one:
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {}; // This line.
};
This line basically prevents an infinite Loop in case both images are missing
The Problem:
I want to test that my onerror function has been called exactly one time. Which I am really stuck on how to do it. Here is the test...
const { container } = render(<Image srcImage={undefined} fallbackImage={undefined} />);
const assertion = container.querySelector('img').onerror;
fireEvent.error(container.firstElementChild);
console.log(container.firstElementChild);
expect(container.firstElementChild.ref.current.onerror).toHaveBeenCalledTimes(1);
// This though has no reference to a real value. Is an example of what I want to get at.
The Question:
How to access the ref callback function and check how many times has my function been called?
Any ideas on this. I am at a loss, I tried mocking refs, I tried mocking and spying on the component. I tried using act and async/await, in case it was called after. I really need some help on this..
You should check if your function is called or not, that's called testing implementation details, rather you should check if your img element have correct src.
Even you should add some alt and user getByAltText to select image element
const { getByAltText } = render(<Image srcImage={undefined} fallbackImage={undefined} />);
const imageElement = getByAltText('Image Alt');
fireEvent.error(imageElement);
expect(imageElement.src).toEqual(imgToFallbackTo);
You have 2 options:
Add a callback to your props that will be called when whenImageIsMissing is called:
export interface ImageProps {
srcImage: string;
classNames?: string;
fallbackImage?: FallbackImages;
onImageMissing?:();
}
const Image = ({
srcImage,
classNames,
onImageMissing,
fallbackImage = FallbackImages.FALLBACK
}: ImageProps) => {
const imgToSourceFrom = srcImage;
const imgToFallbackTo = fallbackImage;
const imageRef = useRef(null);
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {};
if (onImageMissing) onImageMissing();
};
return (
<img ref={imageRef} src={imgToSourceFrom} className={classNames} onError={whenImageIsMissing} />
);
};
and then insert jest.fn in your test and check how many times it was called.
The other option is to take the implementation of whenImageIsMissing and put it inside image.util file and then use jest.spy to get number of calls. Since you are using a function component there is no way to access this function directly.
Hope this helps.

Why is jest.isolateModules not working as expected?

I want to move away from using jest.resetModules since I want to reset two modules, but keep one through all the runs.
My original code to get things working as such was bad like this:
beforeEach(() => {
jest.resetModules();
require('hooks/useMyProvider');
const useMyProvider = require('#/');
api = useMyProvider(); // scoped above
})
Then in each test, I would do something like this:
// Mock hook since we can't spy on a function
jest.doMock('#/hooks/useActive', () => {
return () => {
return [true]
}
});
// re-require the component that requires useActive so it pulls latest mock
let MyComponent = require('#/components/MyComponent')
const { container } = render(<MyComponent />);
I tried refactoring to having useMyProvider defined once at top of file and removing jest.resetModules from the beforeEach;
Then I did this:
let MyComponent;
jest.isolateModules(() => {
jest.doMock('#/hooks/useActive', () => {
return () => {
return [true]
}
});
MyComponent = require('#/components/MyComponent')
});
const { container } = render(<MyComponent />);
When I did this in two tests, and swapped out true for false in the return, whichever test came first would stick. I expected it to change each time since I had isolated the modules.

Resources