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
Related
I want to be able to use a locator variable within all the tests without having to define it every time inside each test.
Something like:
// #ts-check
const { test, expect } = require('#playwright/test');
test.beforeEach( async ({ page }) => {
await page.goto('[desired URL]');
});
// I want to make this variable global to be able to use it within all the tests.
const signInBtn = page.getByTestId('some-button'); // how to resolve 'page' here??
test.describe('My set of tests', () => {
test('My test 1', async ({ page }) => {
await expect(page).toHaveTitle(/Some-Title/);
await expect(signInBtn).toBeEnabled(); // I wanna use the variable here...
});
test('My test 2', async ({ page }) => {
await signInBtn.click(); // ...and here, without having to define it every time inside each test.
});
});
PS: This snippet is just an example to pass the idea, not the actual project, pls don't be attached to it.
You don't have to .Use Page Object Model.. Keep the tests clean.
By using page object model we separate out locator definitions and test method definitions from the actual test to keep it simple, clean & reusable.
See below example:
//Page Object
// playwright-dev-page.js
const { expect } = require('#playwright/test');
exports.PlaywrightDevPage = class PlaywrightDevPage {
/**
* #param {import('#playwright/test').Page} page
*/
constructor(page) {
this.page = page;
this.getStartedLink = page.locator('a', { hasText: 'Get started' });
this.gettingStartedHeader = page.locator('h1', { hasText: 'Installation' });
this.pomLink = page.locator('li', { hasText: 'Guides' }).locator('a', { hasText: 'Page Object Model' });
this.tocList = page.locator('article div.markdown ul > li > a');
}
async goto() {
await this.page.goto('https://playwright.dev');
}
async getStarted() {
await this.getStartedLink.first().click();
await expect(this.gettingStartedHeader).toBeVisible();
}
async pageObjectModel() {
await this.getStarted();
await this.pomLink.click();
}
}
Now we can use the PlaywrightDevPage class in our tests.
// example.spec.js
const { test, expect } = require('#playwright/test');
const { PlaywrightDevPage } = require('./playwright-dev-page');
test('getting started should contain table of contents', async ({ page }) => {
const playwrightDev = new PlaywrightDevPage(page);
await playwrightDev.goto();
await playwrightDev.getStarted();
await expect(playwrightDev.tocList).toHaveText([
`How to install Playwright`,
`What's Installed`,
`How to run the example test`,
`How to open the HTML test report`,
`Write tests using web first assertions, page fixtures and locators`,
`Run single test, multiple tests, headed mode`,
`Generate tests with Codegen`,
`See a trace of your tests`
]);
});
test('should show Page Object Model article', async ({ page }) => {
const playwrightDev = new PlaywrightDevPage(page);
await playwrightDev.goto();
await playwrightDev.pageObjectModel();
await expect(page.locator('article')).toContainText('Page Object Model is a common pattern');
});
You could move it all into a describe block. So something like this should work:
test.describe('My set of tests', () => {
let signInBtn:Locator;
test.beforeEach( async ({ page }) => {
await page.goto('[desired URL]');
signInBtn = page.getByTestId('some-button');
});
test('My test 1', async ({ page }) => {
await expect(page).toHaveTitle(/Some-Title/);
await expect(signInBtn).toBeEnabled();
});
test('My test 2', async ({ page }) => {
await signInBtn.click();
});
});
I have a button click handler in which I call another function. I need to test the function call inside of the handler:
SomeComponent
...
const handler = () => {
someFunction();
}
...
<button data-testId="button" onClick={handler}>Click Me</button>
test
describe('Button click', () => {
it('button click', async () => {
render(<SomeComponent />);
const button = await screen.findByTestId('button');
fireEvent.click(button);
// some silly test case just for example
expect(button).toBeInTheDocument();
});
});
While doing this, it covers the handler but not the inner function itself:
const handler = () => { <<<<<<< covered
someFunction(); <<<<<<< UNCOVERED
}. <<<<<<< covered
The main question here is how can I test the inner function call? If I need to mock it, how should I do it, because the mocked function will not test the actual one?
UPDATE
Also, my someFunction doesn't change anything in the scope of this component, so I can't catch it by comparing the inner state or document change.
SomeFunction is coming from another file and I tested it separately.
It depends on where someFunction is defined. If it's a property given to <SomeComponent /> then you could do something like this:
describe('Button click', () => {
it('button click', async () => {
const someFunction = jest.fn();
render(<SomeComponent someFunction={someFunction} />);
const button = await screen.findByTestId('button');
fireEvent.click(button);
// if there are some precise arguments given to `someFunction` maybe
// use `toHaveBeenCalledWith` instead
expect(someFunction).toHaveBeenCalled();
});
});
But if it's defined in a separate hook then you should mock this hook. For instance here let's assume there a useSomeFunction that directly returns this someFunction:
import { useSomeFunction } from '../path/to/useSomeFunction';
jest.mock('../path/to/useSomeFunction', () => ({
useSomeFunction: jest.fn(),
}));
describe('Button click', () => {
it('button click', async () => {
const mockSomeFunction = jest.fn();
useSomeFunction.mockImplementation(() => mockSomeFunction);
render(<SomeComponent />);
const button = await screen.findByTestId('button');
fireEvent.click(button);
// if there are some precise arguments given to `someFunction` maybe
// use `toHaveBeenCalledWith` instead
expect(mockSomeFunction).toHaveBeenCalled();
});
});
And if it's simply a function defined elsewhere you could adapt the example I gave with hook mocking:
import { someFunction } from '../path/to/util';
jest.mock('../path/to/util', () => ({
someFunction: jest.fn(),
}));
describe('Button click', () => {
it('button click', async () => {
render(<SomeComponent />);
const button = await screen.findByTestId('button');
fireEvent.click(button);
// if there are some precise arguments given to `someFunction` maybe
// use `toHaveBeenCalledWith` instead
expect(someFunction).toHaveBeenCalled();
});
});
someFunction() needs to generate some side effects to your app. You
can test those side effects. For instance if someFunction() was
incrementing a count state value you could test for that in your
component to check if count was incremented when button was clicked.
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);
}, []);
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.
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.