It's my first time to write tests. I'm writing tests for a ReactJS app wrote with hooks, and testing using Jest and react-testing-library.
Here's my functional component:
const ItemDetails = ({ item }) => {
const { code } = item;
const { getBarcode } = useStationContext();
return (
<>
<Button
type="primary"
onClick={() => {
getBarcode(code);
}}
style={{ float: 'right' }}
>
Print Barcode
</Button>
<List
bordered
dataSource={formatData(item)}
renderItem={({ title, value }) => (
<List.Item>
<List.Item.Meta
description={
<Wrapper>
<p>{upperCase(title)}</p>
<div>{value}</div>
</Wrapper>
}
/>
</List.Item>
)}
/>
</>
);
};
export default ItemDetails;
and the test file:
import React from 'react';
import { render, cleanup } from 'react-testing-library';
import ItemDetails from '../containers/Items/ItemPage/ItemDetails';
afterEach(cleanup);
describe('formatData()', () => {
it('Return Details about item', async () => {
const { getByText, getByTestId, container, asFragment } = render(
<ItemDetails
item={{
id: '296-c-4f-89-18',
barcode: 'E-6',
}}
/>,
);
global.console = {
log: jest.fn(getByText, getByTestId, container, asFragment),
};
expect(global.console.log).toHaveBeenCalledWith('test');
});
});
When I run the test I got this error:
TypeError: Cannot read property 'getBarcode' of null
I don't know how can I fix this?
The expectations are wrong because console.log isn't called anywhere. Mocking console object like global.console = ... is a bad practice because it persists between tests and can break things that depend on it, including test runner itself.
There's inconsistency with item code key, it was provided as barcode.
Context value is expected to be undefined, unless default value was provided. It should be provided in test.
It likely should be:
const getBarcodeMock = jest.fn();
const { getByText, getByTestId, container, asFragment } = render(
<StationContext.Provider value={{ getBarcode: getBarcodeMock }}>
<ItemDetails
item={{
id: '296-c-4f-89-18',
code: 'E-6',
}}
/>
</StationContext.Provider>
);
// ...simulate button click...
expect(getBarcodeMock).toBeCalledWith('E-6');
Related
Im trying to get the img element to be printed on the console but for some reason the only thing i get is undefined and null.
This is my code:
import CardContent from '#mui/material/CardContent';
import CardActions from '#mui/material/CardActions';
import UIButton from 'app/main/components/UIButton';
import { useRef, useEffect } from 'react';
function ExpandImageDialog({ open, onClose: close, url }) {
const refInput = useRef();
const styleImage = () => {
console.log(refInput.current, 'it got here');
};
useEffect(() => {
styleImage();
}, [open]);
return (
<Modal open={open} onClose={close}>
<Card className="px-20 pt-6 pb-32" sx={modalStyle}>
<CardHeader title="Visualização de imagem" />
<hr />
<CardContent>
<img
className="flex-img"
loading="lazy"
src={url}
alt="Documento"
style={imageStyle}
ref={refInput} />
</CardContent>
<CardActions className="justify-end">
<UIButton onClick={close}>Voltar</UIButton>
</CardActions>
</Card>
</Modal>
);
}
this is what it shows on the console
Im new to react so sorry if im doing something obviously wrong
Thank you for the help!
The way you are using your ref is correct, see this repro for a simple reproduction. Here is the code :
import React from 'react';
import { render } from 'react-dom';
let renderCount = 0;
const App = () => {
const [isVisible, setIsVisible] = React.useState(false);
const imgRef = React.useRef();
const styleImage = () => {
console.log('img ref = ', imgRef.current);
};
const handleClick = () => {
setIsVisible(!isVisible);
};
React.useEffect(() => {
styleImage();
}, [isVisible]);
renderCount += 1;
return (
<div>
<div>render count = {renderCount}</div>
<button onClick={handleClick}>{isVisible ? 'Hide' : 'Show'} image</button>
<br />
<CustomImg isVisible={isVisible}>
<img src="https://picsum.photos/200/300" ref={imgRef} />
</CustomImg>
<br />
</div>
);
};
// Same as your Modal component
const CustomImg = ({ isVisible, children }) => {
if (!isVisible) return <></>; // Component returns nothing, so img isn't known. Therefore, ref is undefined on the first render.
return <div>{children}</div>;
};
render(<App />, document.getElementById('root'));
The problem comes from the prop open/close that you pass to the Modal component. If it is not rendered, then your ref will stay undefined. The null status surely comes from your Modal as well. In my example, try to show and then hide the image, you will see that the ref is null after you hide it, which is normal. There is a re-render, so the ref is reset as well.
Note that I created the variable renderCount outside of the component, so it is a global variable that you can use to check how many times your component re-render. Try doing it to see what happen.
Your code looks correct.
It is null because you are logging the ref in the useEffect with a dependency on open. This would be running when the component is mounted.
If you try the following with refInput as a dependency, you should see the ref log.
useEffect(()=>{
console.log(refInput.current, 'it got here');
}, [refInput])
I can't give 100% width on the snackbar and I also have a snackbarClose method but I can't implement it on the snackbar. I also want a button 'X' which button will perform the snackbarClose method.
CodeSandbox : https://codesandbox.io/s/xenodochial-kapitsa-f5yd7?file=/src/Demo.js:693-706
import React, { Component } from "react";
import { Container, Grid, Button, Snackbar } from "#material-ui/core";
import MuiAlert from "#material-ui/lab/Alert";
import { withStyles } from "#material-ui/core/styles";
const styles = (theme) => ({});
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
class Demo extends Component {
constructor() {
super();
this.state = {
snackbaropen: false,
snackbarmsg: "",
severity: ""
};
this.onClick = this.onClick.bind(this);
}
onClick = (event) => {
this.setState({
snackbaropen: true,
snackbarmsg: "Data Saved",
severity: "success"
});
};
snackbarClose = (event) => {
this.setState({ snackbaropen: false });
};
render() {
const { classes } = this.props;
return (
<React.Fragment>
<Container>
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={6} lg={6} xl={6}>
<Button
variant="contained"
color="primary"
onClick={this.onClick}
>
Submit
</Button>
<Snackbar
open={this.state.snackbaropen}
autoHideDuration={3000}
onClose={this.snackbarClose}
>
<Alert severity={this.state.severity}>
{this.state.snackbarmsg}
</Alert>
</Snackbar>
</Grid>
</Grid>
</Container>
</React.Fragment>
);
}
}
export default withStyles(styles, { withTheme: true })(Demo);
If you want 100% width on snackbar, you need to specify width for Alert and Snackbar component and for close button you need to specify onClose function on Alert component.
<Snackbar
open={this.state.snackbaropen}
autoHideDuration={3000}
onClose={this.snackbarClose}
style={{ width: "100%" }} // specify width 100%
>
<Alert
onClose={this.snackbarClose} // specify onClose method
severity={this.state.severity}
style={{ width: "100%" }} // specify width 100%
>
{this.state.snackbarmsg}
</Alert>
</Snackbar>
For snackback closing on outside click, you need to change close function like below:-
snackbarClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
this.setState({ snackbaropen: false });
};
Demo: https://codesandbox.io/s/heuristic-khayyam-xo464
This is so complicated to solve problem.
What React version used?
If you used React(over 16.8), I recommend solution I used.
Use React.createContext, React.useContext
Use React.useReducer
Make custom Hook like useSnackBar
Mount SnackBarProvider on the App
import React from 'react';
import ToastContext, { ToastProps } from './ToastContext';
const useToast = () => {
const [, dispatch] = React.useContext(ToastContext);
const message = (toast: ToastProps) => {
dispatch({
type: 'ADD_TOAST',
payload: toast,
});
};
return message;
};
export { useToast };
export default useToast;
const ToastProvider: React.FC<ToastProviderProps> = ({
children,
placement = 'top-right',
timeout = 5000,
}) => {
const [toasts, dispatch] = React.useReducer(toastReducer, []);
return (
<ToastContext.Provider value={[toasts, dispatch]}>
<ToastProviderWrapper>
<ToastProviderContainer className={classnames(placement)}>
{toasts.map((toast, index) => (
<ToastCard {...toast} key={index} timeout={timeout} />
))}
</ToastProviderContainer>
</ToastProviderWrapper>
{children}
</ToastContext.Provider>
);
};
const Index = () => (
<ToastProvider>
<App />
</ToastProvider>
);
const rootElement = document.getElementById('root');
ReactDOM.render(<Index />, rootElement);
You can give data through dispatch(action), reducers
You can Dispatch event all the pages.
These Doms were rendered Root Element like React.Portal. then you can edit global position styles as system.
The SnackBar component delegates to or inherits its style or appearance from its children component, so you can instead adjust the width of the Alert component inside the SnackBar.
<Alert style={{ width: "100%" }} severity={this.state.severity}>
{this.state.snackbarmsg}
</Alert>
This should also adjust the width of the SnackBar component and make it fullwidth.
I am not having this warning probably on the normal functioning of the app but I am having this problem in the unit tests. So I am doing a unit test for a Tabs component and it gives me the following warning:
my jsx file looks like this:
class SimpleTabs extends React.Component {
handleChange = (event, value) => {
const { onChange } = this.props;
onChange(value);
};
render() {
const { classes, selectedChannelIndex } = this.props;
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs
key={selectedChannelIndex}
value={selectedChannelIndex}
onChange={this.handleChange}
classes={{ root: classes.tabsRoot, indicator: classes.tabsIndicator }}
>
{CHANNELS_ARRAY &&
CHANNELS_ARRAY.map((channel, i) => (
<Tab
key={i}
value={i}
label={channel.channel}
classes={{ root: classes.tabRoot, selected: classes.tabSelected }}
/>
))}
</Tabs>
</AppBar>
<TabContainer>{this.props.children}</TabContainer>
</div>
);
}
}
export default withStyles(styles)(SimpleTabs);
and my unit test file looks like this:
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import { configure, mount } from 'enzyme';
import { shallowToJson } from 'enzyme-to-json';
import Tabs from '../../../src/components/common/Tabs/Tabs';
configure({ adapter: new Adapter() });
const defaultProps = { selectedChannelIndex: 0, value: 0, selectInput: 0 };
const setup = (props = {}) => {
const setupProps = { ...defaultProps, ...props };
return shallow(<Tabs {...setupProps} />);
};
describe('Tabs', () => {
const wrapper = mount(<Tabs {...defaultProps} />);
it('should be defined', () => {
expect(Tabs).toBeDefined();
});
it('should render correctly', () => {
const tree = mount(<Tabs />);
expect(shallowToJson(tree)).toMatchSnapshot();
});
});
I've seen others asking about this warning as well but many times it's said that to add the value on the tabs element is the solution but is not working for me.
Problem is not new and I saw similar questions already on stackoverflow but maximum of them are unanswered. Even though test pass, I get this warning. Tried to resolved by wraping act(() => {}); at those statements due to which state changes, but could not identified where exactly I should use it.
Any help will be appreciated.
My Error Message:
● Console
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to CompanyDetail 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.
in CompanyDetail (at CompanyDetail.test.js:53)
in Router (created by BrowserRouter)
in BrowserRouter (at CompanyDetail.test.js:52)
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to CompanyDetail 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.
in CompanyDetail (at CompanyDetail.test.js:53)
in Router (created by BrowserRouter)
in BrowserRouter (at CompanyDetail.test.js:52)
Code For CompanyDetails.js
import React from 'react';
import { Row } from 'react-bootstrap';
import CONSTANTS from '../../constants/constants';
import { Link } from 'react-router-dom';
import useFetch from '../../effects/use-fetch.effect';
import Spinner from '../spinner/Spinner';
const CompanyDetail = (props) => {
const { id } = props.match.params;
const [{ name, description, jobs, known_to_sponsor_visa }, isLoading] = useFetch(
`${CONSTANTS.BASE_URL}/companies/${id}`
);
let classes = 'badge badge-';
classes += known_to_sponsor_visa === 'Yes' ? 'success' : 'danger';
return (
<React.Fragment>
{!isLoading ? (
<Row style={{ margin: '20px' }}>
<div className="col-lg-6 col-md-6">
<div className="company-details">
<h1 data-testid="company-name">{name ? name : 'No name provided'}</h1>
<h5 data-testid="sponsors-visa">
Known to sponsor work visa: {''}
{known_to_sponsor_visa ? <span className={classes}> {known_to_sponsor_visa}</span> : 'No data'}
</h5>
<p data-testid="description">{description ? description : 'No description'}</p>
</div>
</div>
<div className="col-lg-6 col-md-6 ">
<div className="card-box ">
<div className="card-content content-primary">
<h3 className="card-title" data-testid="jobs">
{!jobs ? 'No jobs posted' : ` Last ${jobs.length} Jobs`}
</h3>
<ul>
{jobs
? jobs.map((job) => {
return (
<Link to={`/jobs/${job.job_id}`} data-testid="individual-job-link" key={job.job_id}>
<li data-testid="individual-job" key={job.job_id}>
{job.position}
</li>
</Link>
);
})
: 'No jobs posted'}
</ul>
</div>
</div>
</div>
</Row>
) : (
<div data-testid="spinner">
<Spinner />
</div>
)}
</React.Fragment>
);
};
export default CompanyDetail;
Code for CompanyDetail.test.js
import React from 'react';
import CompanyDetail from '../../../components/company-detail/CompanyDetail';
import { BrowserRouter } from 'react-router-dom';
import { render, waitFor, fireEvent, act } from '#testing-library/react';
describe('Company Details', () => {
let mockData;
let data = {
name: 'Google',
description: 'this is a company description',
known_to_sponsor_visa: 'Yes',
id: '4',
jobs: [{ job_id: '2', position: 'Web developer' }],
};
beforeEach(() => {
jest.spyOn(global, 'fetch').mockResolvedValue({ json: jest.fn().mockResolvedValue({ data }) });
mockData = { match: { params: { id: 4 } } };
});
it('renders company details with given data', async () => {
const { getByTestId } = render(
<BrowserRouter>
<CompanyDetail {...mockData} key={data.jobs[0].job_id} />,
</BrowserRouter>
);
await waitFor(() => expect(getByTestId('company-name').textContent).toMatch('Google'));
await waitFor(() => expect(getByTestId('sponsors-visa').textContent).toMatch('Yes'));
await waitFor(() => expect(getByTestId('description').textContent).toMatch('this is a company description'));
});
it('renders correct jobs length', async () => {
const { getByTestId } = render(
<BrowserRouter>
<CompanyDetail {...mockData} key={data.jobs[0].job_id} />,
</BrowserRouter>
);
await waitFor(() => expect(getByTestId('jobs').textContent).toMatch('1'));
});
it('renders jobs', async () => {
const { getAllByTestId } = render(
<BrowserRouter>
<CompanyDetail {...mockData} key={data.jobs[0].job_id} />,
</BrowserRouter>
);
await waitFor(() => getAllByTestId('individual-job').map((li) => li.textContent));
});
it('renders spinner when there is no jobs', () => {
const { getByTestId } = render(
<BrowserRouter>
<CompanyDetail {...mockData} />,
</BrowserRouter>
);
expect(getByTestId('spinner').children.length).toBe(1);
});
it('navigates to individual job page', async () => {
const { getByText } = render(
<BrowserRouter>
<CompanyDetail {...mockData} />
</BrowserRouter>
);
await waitFor(() => fireEvent.click(getByText(data.jobs[0].position)));
await waitFor(() => expect(document.querySelector('a').getAttribute('href')).toBe(`/jobs/${data.jobs[0].job_id}`));
});
});
Actually I solved it, It was my small mistake. I forget to wrap Spinner test inside async and await. Below code solved it.
it('renders spinner when there is no jobs', async () => {
const { getByTestId } = render(
<BrowserRouter>
<CompanyDetail {...mockData} />,
</BrowserRouter>
);
await waitFor(() => expect(getByTestId('spinner').children.length).toBe(1));
});
When I try to simulate a change event with Enzyme in my Material-UI component, value is not changing.
My component looks like this:
export class EditableList extends Component {
...
onChangeNewEntry = event => {
this.setState({newEntry: event.target.value});
};
render() {
const {classes} = this.props;
return (
<div className={classes.div_main}>
<List>
<Paper className={classes.paper_title} elevation={2} key="NewEntry">
<ListItem>
<InputBase
data-testid='input-base'
className={classes.inputBase}
placeholder="New Entry"
value={this.state.newEntry}
onKeyPress={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.onCreateItem();
}
}}
onChange={this.onChangeNewEntry}
multiline
/>
</ListItem>
</Paper>
</List>
</div>
);
}
}
My test looks like this:
import React from 'react';
import {Provider} from "react-redux";
import EditableList from "./EditableList";
import store from "../../../../store";
import {createMount, createShallow} from "#material-ui/core/test-utils";
import {InputBase} from "#material-ui/core";
let mount;
let wrapper;
beforeEach(() => {
mount = createMount();
wrapper = mount(
<Provider store={store}>
<EditableList/>
</Provider>
);
});
afterEach(() => {
mount.cleanUp();
});
const findComponent = ( value) => {
return wrapper.find(`[data-testid='${value}']`).first();
};
it('allow user to write input for new item', () => {
const inputBase = findComponent('input-base');
const value = "New item";
inputBase.simulate('change', {target: {value: value}});
wrapper.update();
console.log(inputBase.prop('value'));
});
In the console, the valueis always an empty string.
I tried also the following without success:
wrapper.find(InputBase).at(0).props().onChange({target: {value: value}});
wrapper.find(InputBase).at(0).props().onChange({target: {name: 'InputBase', value: value}});