Jest: Continue testing when expected error is thrown on button click - reactjs

I am trying to test a React error boundary (with react-error-boundary library) by creating a button (to test that the code inside the library is displayed) and when clicked I want to verify that the error text is displayed, here are some screenshots
Here are the 2 files used:
error-fallback.tsx
import Box from "#mui/material/Box";
function ErrorFallback({ error }: { error: Error; resetErrorBoundary: () => void }) {
return (
<Box sx={{ textAlign: "center" }}>
<p>Error displaying this section</p>
<pre>{error.message}</pre>
</Box>
);
}
export default ErrorFallback;
error-fallback.test.tsx
import { render, fireEvent } from "#testing-library/react";
import { ErrorBoundary } from "react-error-boundary";
import Box from "#mui/material/Box";
import { useState } from "react";
import ErrorFallback from "./error-fallback";
const buttonText = "Throw Error";
const errorValue = "This is a test error";
const ThrowError = () => {
throw new Error(errorValue);
};
export const ErrorFallbackTest = () => {
const [error, setError] = useState(false);
const triggerError = () => setError(true);
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Box sx={{ textAlign: "center" }}>
{error && <ThrowError />}
<button onClick={triggerError}>{buttonText}</button>
</Box>
</ErrorBoundary>
);
};
it("react boundary works", async () => {
const { getByText } = render(<ErrorFallbackTest />);
const buttonTrigger = getByText(buttonText);
fireEvent(buttonTrigger, new MouseEvent("click", { bubbles: true }));
const boundaryText = getByText("Error displaying this section");
expect(boundaryText).toBeInTheDocument();
expect(boundaryText).toBeDefined();
expect(boundaryText).toBeVisible();
});
Attaching Jest error log. I am not sure which is the proper way to handle this, should I expect an error with Jest? How can I add it if the error is expected to be thrown on button click?
● Console
console.error
Error: Uncaught [Error: This is a test error]

Found the answer here
https://stackoverflow.com/a/54764497/9296982
https://github.com/facebook/react/issues/11098#issuecomment-523977830
it("react boundary works", async () => {
const { getByText } = render(<ErrorFallbackTest />);
const buttonTrigger = getByText(buttonText);
const spy = jest.spyOn(console, "error");
spy.mockImplementation(() => {});
fireEvent(buttonTrigger, new MouseEvent("click", { bubbles: true }));
const boundaryText = getByText("Error displaying this section");
expect(boundaryText).toBeInTheDocument();
expect(boundaryText).toBeDefined();
expect(boundaryText).toBeVisible();
spy.mockRestore();
});

Related

onDidContentSizeChange of Monaco Editor is triggered too often with React

I'm trying to use onDidContentSizeChange of Monaco Editor. Here is the code (CodeSandbox: https://codesandbox.io/s/romantic-pike-8kikk9?file=/src/App.js):
import React, { useRef } from "react";
import Editor from "#monaco-editor/react";
export default function App() {
const editorRef = useRef(null);
function handleEditorDidMount(editor, monaco) {
editorRef.current = editor;
console.log("inside handleEditorDidMount");
editorRef.current.onDidContentSizeChange(() => {
console.log(
"inside onDidContentSizeChange",
editorRef.current.getContentHeight()
);
});
}
return (
<Editor
height="90vh"
defaultLanguage="javascript"
wordWrap="on"
onMount={handleEditorDidMount}
/>
);
}
However, the console shows that the event is triggered too often whenever we type on the editor (sometimes 2 emits for 1 type). Does anyone know what's the reason?
Edit 1: the following code in https://microsoft.github.io/monaco-editor/playground.html does not have this problem.
const width = 300;
const container = document.getElementById('container');
container.style.border = '1px solid #f00';
const editor = monaco.editor.create(container, {
value: "",
language: "javascript",
wordWrap: 'on'
});
editor.onDidContentSizeChange(() => {
console.log(
"inside onDidContentSizeChange",
editor.getContentHeight()
)});
Edit 2: updateOptions({ wordWrap: "on" }) solves the problem, I don't know why setting wordWrap as props does not work. https://codesandbox.io/s/admiring-dew-hdxfel?file=/src/App.js
import Editor from "#monaco-editor/react";
export default function App() {
const editorRef = useRef(null);
function handleEditorDidMount(editor, monaco) {
editorRef.current = editor;
console.log("inside handleEditorDidMount");
editorRef.current.onDidContentSizeChange(() => {
console.log(
"inside onDidContentSizeChange",
editorRef.current.getContentHeight()
);
});
editorRef.current.updateOptions({ wordWrap: "on" });
}
return (
<Editor
height="90vh"
defaultLanguage="javascript"
onMount={handleEditorDidMount}
/>
);
}

Testing-library: removing element with waitForElementToBeRemoved doesn't get jest coverage

Here's a Toast component that is displayed when a button is clicked and disappears after x seconds.
For testing waitFor is used to change showToast state to true when API call is successful, then waitForElementToBeRemoved is used to check if the toast component has been removed from the screen.
Tests are passing, so the assumption is that showToast became false. But when I check the jest coverage, that line hide={() => setShowToast(false)} is still shown as uncovered.
What would be needed to cover that line using testing-library?
Toast component:
const Toast = props => {
const { message, color, iconName, show, hide, background, timeoutDuration, ...rest } = props;
useEffect(() => {
if (show) {
const timeout = setTimeout(() => {
hide();
}, timeoutDuration * 1000 + 1000);
return () => clearTimeout(timeout);
}
}, [show, timeoutDuration]);
return (
<Box>
<Container {...rest} show={show} timeoutDuration={timeoutDuration}>
<StyledToast py={1} px={2} background={background} bgColor={colors[color]} role="alert">
<StyledIcon name={iconName} color={color} />
<StyledP color={color} fontSize={[14, 16]}>
{message}
</StyledP>
</StyledToast>
</Container>
</Box>
);
};
Component that uses Toast:
const [showToast, setShowToast] = useState(false);
{showToast && (
<Toast
message="Store Settings successfully updated!"
color="green"
iconName="check-circle"
background={true}
show={showToast}
timeoutDuration={10}
zIndex={10}
hide={() => setShowToast(false)}
/>
)}
Test:
import '#testing-library/jest-dom';
import { render, screen, fireEvent, waitFor, waitForElementToBeRemoved } from '#testing-library/preact';
test('Clicking OK displays Toast and it disappears', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: {}] } })
})
);
const CheckBox = screen.getByTestId('some-id');
fireEvent.click(CheckBox);
const SaveButton = screen.getByText('Save');
fireEvent.click(SaveButton);
expect(screen.getByText('OK')).toBeTruthy();
const OKButton = screen.getByText('OK');
fireEvent.click(OKButton);
await waitFor(() => {
expect(screen.getByText('Store Settings successfully updated!')).toBeInTheDocument();
}, { timeout: 11000 });
waitForElementToBeRemoved(screen.getByText('Store Settings successfully updated!')).then(() =>
expect(screen.queryByText('Store Settings successfully updated!')).not.toBeInTheDocument()
);
});
Try
await waitForElementToBeRemoved(...)
since waitForElementToBeRemoved is an async function call

React testing extracting assertions jest error

I added eslint to a project and came across a "test has no assertions" error coming from jest/expect-expect. In the code below the render and click have been extracted because of repetition, how do I fix the error besides disabling it for the file?
it('triggers onAssignToClick when the assign button is clicked', () => {
testButtonClick('assign-button', onAssignToClickFn)
})
const testButtonClick = (buttonName, callback) => {
const { getByTestId } = renderComponent()
fireEvent.click(getByTestId(buttonName))
expect(callback).toHaveBeenCalled()
}
const renderComponent = (props) => {
return render(
<Context.Provider value={props.viewport}>
<Actions {...props} />
</Context.Provider>
)
}

How to properly test React Native Modals using Jest and Native Testing Library

I'm having a bit of a hard time understanding how to test my modal component. I'm using the react-native-modals package and #testing-library/react-native with Jest. My component is a modal that pops up when a GraphQL error is passed to it.
./ErrorMessage.js
import React from 'react';
import PropTypes from 'prop-types';
import { Dimensions, Text } from 'react-native';
import Modal, { ModalContent, ScaleAnimation } from 'react-native-modals';
import { theme } from '../styles/theme.styles';
const ModalError = ({ error, onClose }) => {
if (!error || !error.message) {
return (
<Modal visible={false}>
<Text />
</Modal>
);
}
return (
<Modal
visible
modalAnimation={
new ScaleAnimation({
initialValue: 0,
useNativeDriver: true,
})
}
onTouchOutside={onClose}
swipeDirection={['up', 'down', 'left', 'right']}
swipeThreshold={200}
onSwipeOut={onClose}
modalStyle={modalStyle}
overlayOpacity={0.7}
>
<ModalContent>
<Text testID="graphql-error">{error.message}</Text>
</ModalContent>
</Modal>
);
};
ModalError.defaultProps = {
error: {},
};
ModalError.propTypes = {
error: PropTypes.object,
onClose: PropTypes.func.isRequired,
};
export default ModalError;
const window = Dimensions.get('window');
const modalStyle = {
backgroundColor: theme.lightRed,
borderLeftWidth: 5,
borderLeftColor: theme.red,
width: window.width / 1.12,
};
My test is pretty simple so far. I just want to make sure it's rendering the modal. I'm not exactly sure what needs to be mocked out here or how to do it.
./__tests__/ErrorMessage.test.js
import React from 'react';
import { MockedProvider } from '#apollo/react-testing';
import { GraphQLError } from 'graphql';
import { render } from '#testing-library/react-native';
import Error from '../ErrorMessage';
jest.mock('react-native-modals', () => 'react-native-modals');
const error = new GraphQLError('This is a test error message.');
const handleOnCloseError = jest.fn();
describe('<ErrorMessage>', () => {
it('should render an ErrorMessage modal component', () => {
const { container } = render(
<MockedProvider>
<Error error={error} onClose={handleOnCloseError} />
</MockedProvider>
);
expect(container).toMatchSnapshot();
});
});
The error that I'm getting is...
TypeError: _reactNativeModals.ScaleAnimation is not a constructor
18 | visible
19 | modalAnimation={
> 20 | new ScaleAnimation({
| ^
21 | initialValue: 0,
22 | useNativeDriver: true,
23 | })
And the snapshot is only printing...
./__tests__/__snapshots__/ErrorMessage.test.js.snap
// Jest Snapshot v1,
exports[`<ErrorMessage> should render an ErrorMessage modal component 1`] = `
<View
collapsable={true}
pointerEvents="box-none"
style={
Object {
"flex": 1,
}
}
/>
`;
How can I get past this error and make a proper snapshot?
you can use this -> https://github.com/testing-library/jest-native
In react native component,
...
<Modal
testID="test-modal"
deviceWidth={deviceWidth}
deviceHeight={deviceHeight}
isVisible={isModalVisible}. // isModalVisible = useState(true or false)
onBackdropPress={toggleModal}
backdropOpacity={0.5}
>
...
In test component,
...
const test = getByTestId("test-modal");
expect(test).toHaveProp("visible", true); // test success !
...
// components/Example/index.tsx
import React, { useState } from 'react';
import { Pressable, Text } from 'react-native';
import Modal from 'react-native-modal';
const Example: React.FC = () => {
const [isPrivacyPolicyVisible, setIsPrivacyPolicyVisible] = useState(false);
return (
<>
<Pressable onPress={() => setIsPrivacyPolicyVisible(true)}>
<Text>Privacy Policy</Text>
</Pressable>
<Modal
accessibilityLabel="privacy-policy-modal"
isVisible={isPrivacyPolicyVisible}>
<Text>Content</Text>
</Modal>
</>
);
};
export default Example;
// components/Example/index.test.tsx
import React from 'react';
import { fireEvent, render, waitFor } from '#testing-library/react-native';
import { Example } from 'components';
describe('Example Component', () => {
it('should render privacy policy.', async () => {
// Arrange
const { queryByText, queryByA11yLabel } = render(<Example />);
const button = queryByText(/privacy policy/i);
const modal = queryByA11yLabel('privacy-policy-modal');
// Act and Assert
expect(button).toBeTruthy();
expect(modal).toBeTruthy();
expect(modal.props).toMatchObject({
visible: false,
});
});
it('should show privacy policy modal.', async () => {
// Arrange
const { queryByText, queryByA11yLabel } = render(<Example />);
const button = queryByText(/privacy policy/i);
const modal = queryByA11yLabel('privacy-policy-modal');
// Act
await waitFor(() => {
fireEvent.press(button);
});
// Assert
expect(modal.props).toMatchObject({
visible: true,
});
});
});
when you do jest.mock('react-native-modals', () => 'react-native-modals'); you're replacing the whole library with the string 'react-native-modals' thus when you use it in your component it fails. You need to return a full mocked implementation from your mock function (second argument to jest.mock). It's also possible auto-mocking may work for you which would be done by simply doing: jest.mock('react-native-modals');
Here's the docks for jest.mock() with some examples of the various ways to use it: https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options.

How to fix `TypeError: document.createRange is not a function` error while testing material ui popper with react-testing-library?

I have a material-ui TextField which on focus opens a Popper. I am trying to test this behavior using react-testing-library.
Component:
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import Grow from '#material-ui/core/Grow';
import Paper from '#material-ui/core/Paper';
import Popper from '#material-ui/core/Popper';
import TextField from '#material-ui/core/TextField';
import React from 'react';
import { ItemType } from './types';
export type TextFieldDropDownProps = {
items: ItemType,
};
export function TextFieldDropDown(props: TextFieldDropDownProps) {
const [searchTerm, setSearchTerm] = React.useState('');
const [anchorEl, setAnchorEl] = React.useState(null);
const handleSearchTermChange = (event: any) => {
setSearchTerm(event.target.value);
};
const onFoucs = (event: any) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (event: any) => {
setAnchorEl(null);
};
const popperTrans = ({ TransitionProps }: any) => {
return (
<Grow
{...TransitionProps}
style={{ transformOrigin: '0 0 0' }}
>
<Paper>
<ul />
</Paper>
</Grow>
);
};
const open = Boolean(anchorEl);
return (
<ClickAwayListener onClickAway={handleClose}>
<div>
<TextField
onChange={handleSearchTermChange}
onFocus={onFoucs}
value={searchTerm}
label='Search'
/>
<Popper
open={open}
anchorEl={anchorEl}
transition={true}
disablePortal={true}
placement='bottom-start'
style={{zIndex: 999, minWidth: '100%'}}
>
{popperTrans}
</Popper>
</div>
</ClickAwayListener>
);
}
Test:
import { fireEvent, render, wait } from '#testing-library/react';
import { getTestData } from 'test-data';
import React from 'react';
import { TextFieldDropDown } from './TextFieldDropDown';
test('that on focus on input field, the component shows a dropdown', async () => {
// Set up test data
const items: any = getTestData();
// Render component
const props = { items };
const { getByRole, queryByRole } = render(<TextFieldDropDown {...props} />, {});
await wait();
expect(queryByRole('list')).toBeNull();
// Fire event
const placeSelectInputField = getByRole('textbox') as HTMLInputElement;
fireEvent.focus(placeSelectInputField);
// Verify that dropdown is shown
expect(queryByRole('list')).toBeInTheDocument();
});
When I run the test, I get the following error - TypeError: document.createRange is not a function.
The above error occurred in the <div> component:
in div (created by ForwardRef(Portal))
in ForwardRef(Portal) (created by ForwardRef(Popper))
in ForwardRef(Popper) (created by TextFieldDropDown)
in div (created by ForwardRef(ClickAwayListener))
in ForwardRef(ClickAwayListener) (created by TextFieldDropDown)
in TextFieldDropDown
in Provider (created by AllTheProviders)
in AllTheProviders
Consider adding an error boundary to your tree to customize error handling behavior.
The above error occurred in the <ForwardRef(Popper)> component:
in ForwardRef(Popper) (created by TextFieldDropDown)
in div (created by ForwardRef(ClickAwayListener))
in ForwardRef(ClickAwayListener) (created by TextFieldDropDown)
in TextFieldDropDown
in Provider (created by AllTheProviders)
in AllTheProviders
Consider adding an error boundary to your tree to customize error handling behavior.
● that on focus on input field, the component shows a dropdown
TypeError: document.createRange is not a function
46 | // Fire event
47 | const TextFieldComponent = getByRole('textbox') as HTMLInputElement;
> 48 | fireEvent.focus(TextFieldComponent);
| ^
49 |
50 | // Verify that dropdown is shown
51 | expect(queryByRole('list')).toBeInTheDocument();
How do I make this work?
Referring to this github issue, I found out that we can fix the error with following code put in test set up file.
(global as any).document.createRange = () => ({
setStart: () => {},
setEnd: () => {},
commonAncestorContainer: {
nodeName: 'BODY',
ownerDocument: document,
},
});
The issue is with the underlying implementation of PopperJS calling the document.createRange function when there is no DOM API for it to call. The solution is to mock PopperJS.
// __mocks__/popper.js.js
import PopperJs from 'popper.js';
export default class Popper {
constructor() {
this.placements = PopperJs.placements;
return {
update: () => {},
destroy: () => {},
scheduleUpdate: () => {}
};
}
}
jest will pick any mocks in the /__mocks__ directory automatically, so simply adding this file should fix your issue

Resources