Mocking the content of an iframe with React Testing Library - reactjs

I've got a React component that returns an iframe and handles sending messages to and receiving messages from the iframe. I am totally stumped as to how I can mock the iframe and simulate a message being posted in the iframe with React Testing Library. Any ideas or examples would be very helpful.
Component:
const ContextualHelpClient: React.VFC<CHCProps> = ({ onAction, data }) => {
const [iframe, setIframe] = React.useState<HTMLIFrameElement | null>(null);
const handleMessage = React.useCallback((event: MessageEvent<ContextualHelpAction>) => {
onAction(event.data);
}, [onAction])
const contentWindow = iframe?.contentWindow;
React.useEffect(() => {
if (contentWindow) {
contentWindow.addEventListener('message', handleMessage);
return () => contentWindow.removeEventListener('message', handleMessage);
}
}, [handleMessage, contentWindow]);
React.useEffect(() => {
if (contentWindow) {
contentWindow.postMessage({ type: CONTEXTUAL_HELP_CLIENT_DATA, data }, "/");
}
}, [data, contentWindow]);
return <iframe src={IFRAME_SOURCE} width={300} height={300} ref={setIframe} title={IFRAME_TITLE} />;
};
Test:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import ContextualHelpClient from '../ContextualHelpClient';
import { IFRAME_SOURCE, IFRAME_TITLE } from '../constants';
describe('<ContextualHelpClient />', () => {
it('should call onAction when a message is posted', () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE);
fireEvent(iframe.contentWindow, new MessageEvent('data'));
expect(handleAction).toBeCalled(); // fails
});
});

In my case, using the message type on fireEvent was enough to test this.
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import ContextualHelpClient from '../ContextualHelpClient';
import { IFRAME_SOURCE, IFRAME_TITLE } from '../constants';
describe('<ContextualHelpClient />', () => {
it('should call onAction when a message is posted', () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE);
fireEvent(
window,
new MessageEvent('message', {origin: window.location.origin}),
);
expect(handleAction).toBeCalled(); // fails
});
});

Solved!
it('should call onAction when a message is posted', async () => {
const handleAction = jest.fn();
const content = render(<ContextualHelpClient onAction={handleAction} />);
const iframe = content.getByTitle(IFRAME_TITLE) as HTMLIFrameElement;
const TEST_MESSAGE = 'TEST_MESSAGE';
// https://github.com/facebook/jest/issues/6765
iframe.contentWindow?.postMessage(TEST_MESSAGE, window.location.origin);
await waitFor(() => expect(handleAction).toBeCalledWith(TEST_MESSAGE));
});

Related

Why mocking fetch in Jest with beforeAll hook fails while beforeEach does work properly?

I'm practicing mocking with a simple React app using Jest + React Testing Library.
I did successfully mock fetch to test the app's functionality but I'm wondering why setting up the mock with beforeEach hook works while with the beforeAll hook my tests fail.
This is the (working) test code:
import {
render,
screen,
waitForElementToBeRemoved,
} from "#testing-library/react";
import App from "./App";
import { mockFetch } from "./mock-fetch";
import { mockResponse } from "./utils";
beforeEach(() => {
jest.spyOn(window, "fetch").mockImplementation(mockFetch);
});
describe("<App />", () => {
it("renders correctly a title", async () => {
render(<App />);
await waitForLoading();
expect(screen.getByText(/list of posts/i)).toBeInTheDocument();
});
describe("when requesting for posts", () => {
it("renders the list of posts once they are successfully fetched", async () => {
render(<App />);
await waitForLoading();
mockResponse.products.forEach((product) => {
expect(screen.getByText(product.title)).toBeInTheDocument();
expect(screen.getByText(product.price)).toBeInTheDocument();
});
});
it("should render an error message when posts fetch is not successful", async () => {
const error = "Error";
window.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
});
render(<App />);
await waitForLoading();
expect(screen.getByText(error)).toBeInTheDocument();
});
it("should render a no posts message if there are no posts to show", async () => {
window.fetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: () =>
Promise.resolve({
products: [],
}),
});
render(<App />);
await waitForLoading();
expect(screen.getByText(/no posts.../i)).toBeInTheDocument();
});
});
});
function waitForLoading() {
return waitForElementToBeRemoved(() => screen.queryByText(/loading.../i));
}
This is the App component:
import { useState, useEffect } from "react";
import Products from "./Products";
import "./App.css";
import { fetchProducts } from "./fetch-products";
function App() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
fetchProducts()
.then((results) => {
setProducts(results.products);
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (error) return <div>{error}</div>;
return (
<div>
<h1>List of posts</h1>
{loading ? <p>Loading...</p> : <Products products={products} />}
</div>
);
}
export default App;
When using beforeAll instead of beforeEach the assertions fail because it can't find the elements I'm querying for. Also, I noticed that the fetchProducts function throws, this error is being set on the error state and in the test it's rendering the div containing the error with the message: Cannot read properties of undefined (reading 'ok').

How can I run a mutateFunction onClick in my jest test code and get its value?

What I want to do
We have created hooks that communicate useMutation and switch the display when a button is clicked in a React component.
This test code is created using jest.
Occurring problems
Error: Uncaught [TypeError: Cannot read properties of undefined (reading '0')]
Source code in question
# Todo.jsx
import React, { useState } from 'react';
export const Todo = () => {
const [token, setToken] = useState('')
const [mutateFunction] = useMutation(CREATE_TOKEN);
const changeTodo = async (agreement) => {
const createTokenData = await mutateFunction();
if (createTokenData.data?.token === null) {
setToken('')
return;
}
setToken(createTokenData.data?.token)
}
};
return (
<div>
<button onClick={() => changeTodo(true)}>
Change Todo
</button>
</div>
)
};
# Todo.test.jsx
import React from 'react';
import { MockedProvider } from '#apollo/client/testing';
import { render, screen, fireEvent } from '#testing-library/react';
import { Todo } from 'Todo'
jest.mock('#apollo/client');
describe('Click the button', () => {
test('The value of token is null', async () => {
const mocks = [
{
request: {
query: CREATE_TOKEN,
},
result: {
data: {
createPcfToken: null
},
},
},
];
render(
<MockedProvider mocks={mocks} addTypename={false}>
<Todo />
</MockedProvider>
);
const button = screen.getByRole('button');
fireEvent.click(button);
});
});
What we tried
I am creating it while referring to the apollo official website, but I cannot receive data from mutateFunction.
I have tried everything but it just fails. What should I do?
Two mockImplementation solved the problem!
const mocks = {
{
data: {
token: null
},
};
}
const mockMutation = jest.fn()
(useMutation as jest.Mock).mockImplementation(() => [mockMutation]);
(mockMutation as jest.Mock).mockImplementation(() => mocks);

how to test a useEffect inside custom hook that create a script tag dynamically

I have a custom hook called useScript:
import { useEffect } from 'react';
const useScript = scriptUrl => {
useEffect(() => {
const script = document.createElement('script');
script.src = scriptUrl;
script.async = true;
document.body.appendChild(script);
return () => document.body.removeChild(script);
}, [scriptUrl]);
};
export default useScript;
And I want to test it. I'm traying this way:
import React from "react";
import { renderHook } from "#testing-library/react-hooks";
import useScript from ".";
describe("useScript tests", () => {
it('verify that the script tag is created', () => {
const wrapper = ({children}) => <body>{children}</body>;
const initialProps = {
scriptUrl: 'https://crm.zoho.com/crm/javascript/zcga.js'
};
const { result } = renderHook(
() => useScript('https://crm.zoho.com/crm/javascript/zcga.js'),
{
initialProps,
wrapper
},
);
});
});
I don't know if I'm going the right way
This way:
import React from "react";
import useScript from ".";
import { render, } from '#testing-library/react';
describe("useScript tests", () => {
it('verify that the script tag is created', () => {
const scriptUrl = 'https://crm.zoho.com/crm/javascript/zcga.js';
const WrapperComponent = () => {
useScript(scriptUrl);
return null;
};
render(<WrapperComponent /> );
const script = document.body.querySelector(`script[src="${scriptUrl}"]`);
expect(script.src).toBe(scriptUrl);
});
});

Warning: An update to App inside a test was not wrapped in act(...) in enzyme and hooks

I have written this component. it fetchs data using hooks and state. Once it is fetched the loading state is changed to false and show the sidebar.
I faced a problem with Jest and Enzyme, as it does throw a warning for Act in my unit test. once I add the act to my jest and enzyme the test is failed!
// #flow
import React, { useEffect, useState } from 'react';
import Sidebar from '../components/Sidebar';
import fetchData from '../apiWrappers/fetchData';
const App = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const getData = async () => {
try {
const newData = await fetchData();
setData(newData);
setLoading(false);
}
catch (e) {
setLoading(false);
}
};
getData();
// eslint-disable-next-line
}, []);
return (
<>
{!loading
? <Sidebar />
: <span>Loading List</span>}
</>
);
};
export default App;
And, I have added a test like this which works perfectly.
import React from 'react';
import { mount } from 'enzyme';
import fetchData from '../apiWrappers/fetchData';
import data from '../data/data.json';
import App from './App';
jest.mock('../apiWrappers/fetchData');
const getData = Promise.resolve(data);
fetchData.mockReturnValue(getData);
describe('<App/> Rendering using enzyme', () => {
beforeEach(() => {
fetchData.mockClear();
});
test('After loading', async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
wrapper.update();
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
});
So, I got a warning:
Warning: An update to App 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 */
});
I did resolve the warning like this using { act } react-dom/test-utils.
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import fetchData from '../apiWrappers/fetchData';
import data from '../data/data.json';
import App from './App';
jest.mock('../apiWrappers/fetchData');
const getData = Promise.resolve(data);
fetchData.mockReturnValue(getData);
describe('<App/> Rendering using enzyme', () => {
beforeEach(() => {
fetchData.mockClear();
});
test('After loading', async () => {
await act(async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
wrapper.update();
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
});
});
But, then my test is failed.
<App/> Rendering using enzyme › After loading
expect(received).toEqual(expected) // deep equality
Expected: false
Received: true
35 |
36 | wrapper.update();
> 37 | expect(wrapper.find('span').exists()).toEqual(false);
Does anybody know why it fails? Thanks!
"react": "16.13.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
This issue is not new at all. You can read the full discussion here: https://github.com/enzymejs/enzyme/issues/2073.
To sum up, currently in order to fix act warning, you have to wait a bit before update your wrapper as following:
const waitForComponentToPaint = async (wrapper) => {
await act(async () => {
await new Promise(resolve => setTimeout(resolve));
wrapper.update();
});
};
test('After loading', async () => {
const wrapper = mount(<App />);
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
// before the state updated
await waitForComponentToPaint(wrapper);
// after the state updated
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});
You should not wrap your whole test in act, just the part that will cause state of your component to update.
Something like the below should solve your problem.
test('After loading', async () => {
await act(async () => {
const wrapper = mount(<App />);
});
expect(wrapper.find('span').at(0).text()).toEqual('Loading List');
const d = await fetchData();
expect(d).toHaveLength(data.length);
await act(async () => {
wrapper.update();
})
expect(wrapper.find('span').exists()).toEqual(false);
expect(wrapper.html()).toMatchSnapshot();
});

How can I test a React Hooks component by changing useState

I have a React hooks functional component that I'd like to test with Jest/Enzyme. I would like test its tertiary render behaviour based upon a useState value. I can't seem to find any example online. There is no 'click' to simulate - no API call to mock because at the end, I still need to test based upon the useState value.
In the past, with class components, I could set the state. With the new hooks, I can't.
So, basically - how do I mock an async await inside a mocked submitForm function so that the render behaves properly?
Here's my component:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
import Form from 'core/Form';
export const Parent = ({submitForm}) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = name => evt => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
Here's my testing so far:
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import Parent from './Parent';
import Form from 'core/Form';
let wrapper, props;
.
.
.
describe('<Parent /> rendering', () => {
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<Parent {...props} />);
});
afterEach(() => {
jest.clearAllMocks();
});
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(init => [init, setState]);
it('Should render 1 Form', () => {
expect(wrapper.find(Form)).toHaveLength(1);
});
it('renders Redirect after API call', () => {
setRedirect = jest.fn(() => false);
expect(wrapper.find(Redirect)).toHaveLength(1);
});
it('renders Form before API call', () => {
setRedirect = jest.fn(() => true);
expect(wrapper.find(Form)).toHaveLength(1);
});
});
You don't need to spy useState hook. Which means you should not test these hooks and methods of the component directly. Instead, you should test components' behavior(the state, props and what is rendered)
E.g.
index.tsx:
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
export const Form = ({ onSubmit, onChange, values }) => <form onSubmit={onSubmit}></form>;
const path = '/user';
export const Parent = ({ submitForm }) => {
const [formValues, setFormValues] = useState({});
const [redirect, setRedirect] = useState(false);
const handleChange = (name) => (evt) => {
setFormValues({ ...formValues, [name]: evt.target.value });
};
const onSubmit = async () => {
try {
const res = await submitForm(formValues);
if (res) setRedirect(true);
else setRedirect(false);
} catch (err) {
console.log('Submit error: ', err);
}
};
return redirect ? (
<Redirect push to={path} />
) : (
<Form onSubmit={onSubmit} values={formValues} onChange={handleChange} />
);
};
export default Parent;
index.test.tsx:
import Parent, { Form } from './';
import React from 'react';
import { shallow } from 'enzyme';
import { Redirect } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
const whenStable = async () =>
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
describe('60137762', () => {
it('should render Form', () => {
const props = { submitForm: jest.fn() };
const wrapper = shallow(<Parent {...props}></Parent>);
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle submit and render Redirect', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(true) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Redirect)).toBeTruthy();
});
it('should handle submit and render Form', async () => {
const props = { submitForm: jest.fn().mockResolvedValueOnce(false) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(wrapper.find(Form)).toBeTruthy();
});
it('should handle error if submit failure', async () => {
const logSpy = jest.spyOn(console, 'log');
const mError = new Error('network');
const props = { submitForm: jest.fn().mockRejectedValueOnce(mError) };
const wrapper = shallow(<Parent {...props}></Parent>);
wrapper.find(Form).simulate('submit');
await whenStable();
expect(props.submitForm).toBeCalledWith({});
expect(logSpy).toHaveBeenCalledWith('Submit error: ', mError);
});
});
Unit test results with coverage report:
PASS stackoverflow/60137762/index.test.tsx
60137762
✓ should render Form (18ms)
✓ should handle submit and render Redirect (15ms)
✓ should handle submit and render Form (8ms)
✓ should handle error if submit failure (18ms)
console.log node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
Submit error: Error: network
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:39:20
at step (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:44:23)
at Object.next (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:25:53)
at /Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:19:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:15:12)
at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/stackoverflow/60137762/index.test.tsx:37:47)
at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:100:37)
at resolve (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
at new Promise (<anonymous>)
at mapper (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
at promise.then (/Users/ldu020/workspace/github.com/mrdulin/react-apollo-graphql-starter-kit/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 78.57 | 100 | 40 | 93.75 |
index.tsx | 78.57 | 100 | 40 | 93.75 | 12
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 3.716s, estimated 5s
Source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60137762

Resources