testing with jest to update a react state inside a rejected promise - javascript

This is a continuation of this question. I have made a few changes that simplifies the question(I believe) and changes it drastically.
I have seperated creating the hook and initialization of midi events.
describe("midiConnection", () => {
it("Should fail", () => {
const midiPorts = renderHook(() => { return MidiConnection()})
act(() => {
midiPorts.result.current.enable()
})
console.log(midiPorts.result.current.error)
})
})
export function MidiConnection() {
const {array: midiInputs, push: midiPush, filter: midiFilter} = useArray(["none"])
const [error, setError] = useState<Error | undefined>();
function enable() {
WebMidi.addListener("connected", (e) => { if (isInput(e)) {midiPush(e.port.name)}});
WebMidi.addListener("disconnected", (e) => {
e.port.removeListener()
if (isInput(e)) {midiFilter((str) => {return str != e.port.name})}
});
// setError("this is a test")
WebMidi.
enable().
catch((err) => {
// console.log("test")
// setError(err)
})
}
return ({ports: midiInputs, error, enable})
}
the warning is still;
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
In addition to seperating out some of the logic I have also experimented with placing setError() on other lines to see if I can trigger the warning (the commented out comments.)
It appears that the warning is only triggered when I try to update the state when the promise from enable() is rejected.
What can I do to stop this error from happening?
EDIT: I have created a working replica of this in CodeSandbox, which you will see if you go to tests and look at the console.

Your hook is async so u need to wait for the next update. Here is the docs that talks more about it.
import { renderHook, act } from "#testing-library/react-hooks/dom";
import CHook from "./../hook/CHook";
test("This is a test", async () => {
const { result, waitForNextUpdate } = renderHook(() => CHook());
act(() => {
result.current.update();
});
await waitForNextUpdate();
console.log(result.current.error);
});
Here is the link to a fixed sandbox.

Related

How can I use async function in hardwareBackPress listener?

I made an application that can make agora voice calls with React native. When clicking leave on the screen, the below agoraEnd function works correctly. I also want this function to work when the phone's back button is pressed. That's why I prepared the useFocusEffet below. But when I press the back button during a voice call, I get the following error. Why is this function not working correctly? How can I fix this problem.
Possible Unhandled Promise Rejection (id: 2): TypeError: engine.leaveChannel is not a function. (In 'engine.leaveChannel()', 'engine.leaveChannel' is undefined)
const agoraEnd = async () => {
await engine.leaveChannel()
await engine.destroy()
navigation.goBack();
};
useFocusEffect(
useCallback(() => {
const backHandler = BackHandler.addEventListener(
'hardwareBackPress',
agoraEnd,
);
return () => backHandler.remove();
}, []),
)
Based on your code, you don't return anything from back handler (though it expects boolean, where true when you handled back press and false when you didn't and want to continue default behaviour (usually exiting the app))
You can just do:
useFocusEffect(
useCallback(() => {
const backHandler = BackHandler.addEventListener(
'hardwareBackPress',
() => agoraEnd(),
);
return () => backHandler.remove();
}, []),
)
if you don't want to wait for agoraEnd to finish.
So in my case I used BackHandler.exitApp(); to do async stuff:
const handleBackButtonAsync = async () => {
// do some await stuff
if (something) {
// the back button event is handled;
} else {
// THE MAGIC:
// instead of returning `false` in `handleBackButton`
// to not prevent default logic, you exit app explicitly
BackHandler.exitApp();
}
}
const handleBackButton = () => {
handleBackButtonAsync();
return true; // always "handle" the back button event to not exit app
}
useEffect(() => {
BackHandler.addEventListener("hardwareBackPress", handleBackButton);
return () =>
BackHandler.removeEventListener("hardwareBackPress", handleBackButton);
}, []);

React `act` warning on DidMount and Promise

Okay so I have this bunch of code that's is thrown on useEffect(() => {...}, []) a.k.a componentDidMount.
// utils/apiCalls.ts
export const loadData = async <T>(
url: string,
errorMsg = "Couldn't retrieve data.",
): Promise<T> => {
const res = await fetch(url, { mode: 'cors', credentials: 'include' });
if (res.ok) return await res.json();
throw new Error(errorMsg);
};
export const loadChat = (id: number): Promise<IChat> => {
return loadData<IChat>(
`${CHAT_API}/${id}/nested/`,
"We couldn't get the chat.",
);
};
// components/MessageContainer.tsx
const MessageContainer = (/* props */) => {
/*
* Some coding...
*/
useEffect(() => {
if (session === null) return;
if (chat === null) {
loadChat(session.chat).then(setChat).catch(alert);
return;
}
// More coding...
}, [session, chat]);
};
The problem comes when I try to test it with #testing-library/react since it gives me this warning Warning: An update to MessagesContainer inside a test was not wrapped in act(...).
How can I make a correct test for this?
Here's the test I have right now.
// tests/MessagesContainer.spec.tsx
describe('MessagesContainer suite', () => {
it('loads messages on mount', () => {
fetchMock.mockResponseOnce(JSON.stringify(ChatMock));
render(
<SessionContext.Provider value={SessionMock}>
<MessagesContainer {...MessagesContainerMockedProps} />
</SessionContext.Provider>,
);
expect(fetchMock.mock.calls.length).toEqual(1);
});
});
NOTE: Wrapping render on act did not work.
fetch is async function which finished only all regular script execution ends. You need to really await fetch finished before call expect.
Moreover, it is not recommend to use testing-library as you did. You want to check how element rendered after fetch, write you test accordance to exception result in UI.
For instance, if after fetching you expect something like this:
<span>message</span>
you expect span with message, and test will be:
expect(screen.findByText('message'));
So at the end I used waitFor in order to check if element has been mounted.
// tests/ChatMessagesContainer.spec.tsx
describe('ChatMessagesContainer suite', () => {
it('renders multiple messages', async () => {
const messageMock = Object.assign({}, MessageMock);
messageMock.message = faker.lorem.words();
const chatMock = Object.assign({}, ChatMock);
chatMock.chat_message_set = [MessageMock, messageMock];
fetchMock.mockResponseOnce(JSON.stringify(chatMock));
const { getAllByText } = render(
<AuthContext.Provider value={UserMock}>
<SessionContext.Provider value={SessionMock}>
<MessagesContainer {...MessagesContainerMockedProps} />
</SessionContext.Provider>
</AuthContext.Provider>,
);
// THIS IS THE IMPORTANT PART
expect(await screen.findByText(MessageMock.message)).toBeInTheDocument();
expect(getAllByText(MessageMock.author.username).length).toEqual(2);
});
});
This article was really useful Maybe you don't need act.

converting class to hooks getting Property 'then' does not exist on type '(dispatch: any) => Promise<void>'.ts(2339)

I'm new to react, here I have two same codes, one is with classes that work, and another is converted from that same class into hooks.
in hooks version, my 'then' is giving an error
Property 'then' does not exist on type '(dispatch: any) =>
Promise'.ts(2339)
have I made some mistake with conversion?
why it is not giving the same error in class while both are the same?
also console.log("Fetched model", realGraph.model); should give an object but it is giving undefined(in-class version it works), but if I put this console outside of loadGraph function then it gives an object, why it's not giving an object inside loadGraph function?
any ideas and suggestions?
class:
import { getGraph, getFloorplan, changeActiveCamera } from '../redux/actions';
const mapStateToProps = (state) => {
return {
currentSite: state.selection.currentSite,
currentCamera: state.selection.currentCamera,
};
};
function mapDispatchToProps(dispatch) {
return {
getGraph: (site) => dispatch(getGraph(site)),
getFloorplan: (site) => dispatch(getFloorplan(site)),
changeActiveCamera: (site, id) => dispatch(changeActiveCamera(site, id)),
};
}
loadGraph() {
if (this.props.currentSite) {
this.props.getFloorplan(this.props.currentSite.identif).then(() => {
console.log('Fetched floorplan');
this.props.getGraph(this.props.currentSite.identif).then(() => {
console.log('Fetched model', this.props.realGraph.model);
// new camera-related node & link status
if (this.props.currentCamera) {
this.props.changeActiveCamera(
this.props.currentSite.identif,
this.props.currentCamera.identif
);
}
});
});
}
}
converted from class to hooks:
Hooks:
const dispatch = useDispatch();
const realGraph = useSelector((state) => state.graphArticles.graph);
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const dispatchGetFloorplan = (site) => dispatch(getFloorplan(site));
const dispatchGetGraph = (site) => dispatch(getGraph(site));
const dispatchChangeActiveCamera = (site, id) =>
dispatch(changeActiveCamera(site, id));
const loadGraph = () => {
if (currentSite) {
dispatchGetFloorplan(currentSite.identif).then(() => {
console.log('Fetched floorplan');
dispatchGetGraph(currentSite.identif).then(() => {
console.log('Fetched model', realGraph.model);
// new camera-related node & link status
if (currentCamera) {
dispatchChangeActiveCamera(
currentSite.identif,
currentCamera.identif
);
}
});
});
}
};
my action related to those:
export function getGraph(site) {
return getData(`api/graph/${site}`, GET_GRAPHS);
}
export function getFloorplan(site) {
return getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
}
On first glance, there are several things I would change in the code you provided.
First, don't use any wrapper factories over your dispatch functions. Use dispatch(action()) directly where you need it component. You aren't gaining anything by creating wrapper functions.
Second, it would be advisable to use some sort of middleware, like Redux Thunk, to handle async Redux actions (like fetching something from the API).
The actions you provided are just "dumb" functions, which are not returning promises so you can't expect it to be "then"-able in your target component.
I also advise the async/await syntax since it is much more readable.
Third, you need to leverage the Hooks reactive API with the useEffect hook.
So first try to define getFloorPlan and getGraph as async actions using the redux-thunk syntax.
export const getGraphAsync = (site) => async (dispatch) => {
try {
const data = await getData(`api/graph/${site}`, GET_GRAPHS);
dispatch(saveGraphData(data)) // save data into Redux store with a normal, synchronous action (plain object)
} catch (error) {
console.log(error)
}
}
export const getFloorplanAsync = (site) => async (dispatch) => {
try {
const data = await getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
dispatch(saveImageData(data)) // save data into Redux store with a normal, synchronous action (plain object)
} catch (error) {
console.log(error)
}
}
I am making an assumption that you correctly configured your store.js to use the thunk middleware.
And then refactor the rest of the component (following some best practices):
const someHookComponent = () => {
// ...
const dispatch = useDispatch();
const currentSite = useSelector((state) =>
state.selection.currentSite);
const currentCamera = useSelector((state) =>
state.selection.currentCamera);
const loadGraph = async () => {
if (currentSite) {
await dispatch(getFloorPlanAsync(currentSite.identif));
console.log('Fetched floorplan');
await dispatch(getGraphAsync(currentSite.identif));
console.log('Fetched model', realGraph.model); /* where is
realGraph coming from? */
/* Why is it important that these 2 dispatches follow one
another when there is no data being passed from one to the
other, or being used later in the component... */
});
});
}
};
useEffect(() => {
// new camera-related node & link status
if (currentCamera) {
dispatch(changeActiveCamera(
currentSite.identif,
currentCamera.identif
));
}
}, [currentSite?.identif, currentCamera?.identif]) /* null chaining is optional here */
// ...
}
I am guessing that loadGraph gets called by some onClick event somewhere down the line like this:
onClick={loadGraph}
If it is called inside useEffect, define the deps (variables used inside loadGraph):
useDeepCompareEffect(() => {
// ... some logic
loadGraph()
}, [currentSite, realGraph])
If you put your currentSite and currentCamera objects directly into the useEffect list of deps then you need to do a deep comparison "by hand".
In that case it's best to create a custom hook like useDeepCompareEffect which will do the heavy lifting of running deep comparisons of reference types under the hood (with the help of some library like lodash for example).
If you want to use or console.log the latest value of realGraph (reference type), you need to use the useEffect hook with a deep comparison again (or just extract the target primitive directly into the deps list and use vanilla useEffect) :
useDeepCompareEffect(() => {
if (realGraph) {
console.log('Fetched model', realGraph.model);
}
}, [realGraph]) // reference type
// or
useEffect(() => {
if (realGraph) {
console.log('Fetched model', realGraph.model);
}
}, [realGraph.someProperty]) // primitive

React Testing Library - not wrapped in act() error

I'm having trouble using react-testing-library to test a toggle component.
On click of an icon (wrapped in a button component), I am expecting text to go from 'verified' to 'unverified'. In addition, a function is called where there are state updates.
However, the click event doesn't seem to work, and I am getting the below error:
> jest "MyFile.spec.tsx"
FAIL src/my/path/__tests__/MyFile.spec.tsx
component MyFile
✓ renders when opened (94 ms)
✓ renders with items (33 ms)
✕ toggles verification status on click of icon button (100 ms)
console.error
Warning: An update to MyFile inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at MyFile (/path/to/myfile.tsx:44:3)
at ThemeProvider (/users/node_modules/#material-ui/styles/ThemeProvider/ThemeProvider.js:48:24)
123 | );
124 | } finally {
> 125 | setIsLoading(false);
| ^
126 | }
127 | };
128 |
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-dom/cjs/react-dom.development.js:24064:9)
at dispatchAction (node_modules/react-dom/cjs/react-dom.development.js:16135:9)
at handleConfirm (src/modules/myfile.tsx:125:7)
In my code, I have a function like this:
const handleSubmit = async() => {
if(isLoading) {
return;
}
try {
setIsLoading(true);
await myFunctionCalls();
} catch (error){
console.log(error)
} finally {
setIsLoading(false)
}
};
My test looks similar to this:
test('toggles verification status on click of icon button', async () => {
renderWithTheme(
<MyComponent/>,
);
const updateVerificationMock = jest.fn();
const callFunctionWithSerializedPayloadMock =
callFunctionWithSerializedPayload as jest.Mock;
callFunctionWithSerializedPayloadMock.mockImplementation(
() => updateVerificationMock,
);
const button = screen.getByRole('button', {name: 'Remove approval'});
fireEvent.click(button);
await act(async () => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1);
});
expect(await screen.findByText('unverified')).toBeInTheDocument();
});
The first expect passes as the function calls are called once, however I have the act() error from above, and also there is a failure as it seems that the text does not toggle from verified to unverified.
I am aware that usually the act error is an issue of async/waiting for calls to happen, but I thought that findByText should wait, and it seems like there is another issue I'm not catching here. Any help on what to do to debug/improve this test?
There are 3 async functions that are called here when you click on the Remove Approval button.
First, you are setting the loading state to true, so it will load then the async function (myFunctionCalls) is called, and finally, the loader will disappear after the loading state is set to false.
In order to solve it, we have to wait for the loading to appear first, then myFunctionCalls is called, and then later we have to wait for loading to disappear.
test("toggles verification status on click of icon button", async () => {
renderWithTheme(<MyComponent />);
const updateVerificationMock = jest.fn();
const callFunctionWithSerializedPayloadMock =
callFunctionWithSerializedPayload as jest.Mock;
callFunctionWithSerializedPayloadMock.mockImplementation(
() => updateVerificationMock
);
const button = screen.getByRole("button", { name: "Remove approval" });
fireEvent.click(button);
expect(await screen.findByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1);
});
await waitForTheElementToBeRemoved(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
expect(await screen.findByText("unverified")).toBeInTheDocument();
});
If you do not have loading text then you can use act(() => jest.advanceTimersByTime(500)); for extending the time till 500ms. When the time reaches 500ms, the async function would have been resolved.
beforeEach(() => {
jest.useFakeTimers();
})
afterEach(() => {
jest.runAllPendingTimers();
jest.useRealTimers()
})
test("toggles verification status on click of icon button", async () => {
renderWithTheme(<MyComponent />);
const updateVerificationMock = jest.fn();
const callFunctionWithSerializedPayloadMock =
callFunctionWithSerializedPayload as jest.Mock;
callFunctionWithSerializedPayloadMock.mockImplementation(
() => updateVerificationMock
);
const button = screen.getByRole("button", { name: "Remove approval" });
fireEvent.click(button);
act(() => jest.advanceTimersByTime(500));
await waitFor(() => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1);
});
act(() => jest.advanceTimersByTime(500));
expect(await screen.findByText("unverified")).toBeInTheDocument();
});
Try this:
// [...]
fireEvent.click(button);
await waitFor(() => {
expect(myFunctionCalls).toHaveBeenCalledTimes(1),
expect(screen.findByText('unverified')).toBeInTheDocument()
});
// End of test

How do I setup this JS code to do better testing?

Hi guys I'm having trouble testing the below JS using Jest. It starts with waitForWorker. if the response is 'working' then it calls waitForWorker() again. I tried Jest testing but I don't know how to test an inner function call and I've been researching and failing.
const $ = require('jquery')
const axios = require('axios')
let workerComplete = () => {
window.location.reload()
}
async function checkWorkerStatus() {
const worker_id = $(".worker-waiter").data('worker-id')
const response = await axios.get(`/v1/workers/${worker_id}`)
return response.data
}
function waitForWorker() {
if (!$('.worker-waiter').length) {
return
}
checkWorkerStatus().then(data => {
// delay next action by 1 second e.g. calling api again
return new Promise(resolve => setTimeout(() => resolve(data), 1000));
}).then(worker_response => {
const working_statuses = ['queued', 'working']
if (worker_response && working_statuses.includes(worker_response.status)) {
waitForWorker()
} else {
workerComplete()
}
})
}
export {
waitForWorker,
checkWorkerStatus,
workerComplete
}
if (process.env.NODE_ENV !== 'test') $(waitForWorker)
Some of my test is below since i can't double check with anyone. I don't know if calling await Worker.checkWorkerStatus() twice in the tests is the best way since waitForWorker should call it again if the response data.status is 'working'
import axios from 'axios'
import * as Worker from 'worker_waiter'
jest.mock('axios')
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() }
})
});
beforeEach(() => jest.resetAllMocks() )
afterEach(() => {
jest.restoreAllMocks();
});
describe('worker is complete after 2 API calls a', () => {
const worker_id = Math.random().toString(36).slice(-5) // random string
beforeEach(() => {
axios.get
.mockResolvedValueOnce({ data: { status: 'working' } })
.mockResolvedValueOnce({ data: { status: 'complete' } })
jest.spyOn(Worker, 'waitForWorker')
jest.spyOn(Worker, 'checkWorkerStatus')
document.body.innerHTML = `<div class="worker-waiter" data-worker-id="${worker_id}"></div>`
})
it('polls the correct endpoint twice a', async() => {
const endpoint = `/v1/workers/${worker_id}`
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint]])
expect(data).toMatchObject({"status": "working"})
})
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint],[endpoint]])
expect(data).toMatchObject({"status": "complete"})
})
})
it('polls the correct endpoint twice b', async() => {
jest.mock('waitForWorker', () => {
expect(Worker.checkWorkerStatus).toBeCalled()
})
expect(Worker.waitForWorker).toHaveBeenCalledTimes(2)
await Worker.waitForWorker()
})
I think there are a couple things you can do here.
Inject status handlers
You could make the waitForWorker dependencies and side effects more explicit by injecting them into the function this lets you fully black box the system under test and assert the proper injected effects are triggered. This is known as dependency injection.
function waitForWorker(onComplete, onBusy) {
// instead of calling waitForWorker call onBusy.
// instead of calling workerComplete call onComplete.
}
Now to test, you really just need to create mock functions.
const onComplete = jest.fn();
const onBusy = jest.fn();
And assert that those are being called in the way you expect. This function is also async so you need to make sure your jest test is aware of the completion. I notice you are using async in your test, but your current function doesnt return a pending promise so the test will complete synchronously.
Return a promise
You could just return a promise and test for its competition. Right now the promise you have is not exposed outside of waitForWorker.
async function waitForWorker() {
let result = { status: 'empty' };
if (!$('.worker-waiter').length) {
return result;
}
try {
const working_statuses = ['queued', 'working'];
const data = await checkWorkerStatus();
if (data && working_statuses.includes(data.status)) {
await waitForWorker();
} else {
result = { status: 'complete' };
}
} catch (e) {
result = { status: 'error' };
}
return result;
}
The above example converts your function to async for readability and removes side effects. I returned an async result with a status, this is usefull since there are many branches that waitForWorker can complete. This will tell you that given your axios setup that the promise will complete eventually with some status. You can then use coverage reports to make sure the branches you care about were executed without worrying about testing inner implementation details.
If you do want to test inner implementation details, you may want to incorporate some of the injection principals I mentioned above.
async function waitForWorker(request) {
// ...
try {
const working_statuses = ['queued', 'working'];
const data = await request();
} catch (e) {
// ...
}
// ...
}
You can then inject any function into this, even a mock and make sure its called the way you want without having to mock up axios. In your application you simply just inject checkWorkerStatus.
const result = await waitForWorker(checkWorkerStatus);
if (result.status === 'complete') {
workerComplete();
}

Categories