I'm trying to test that a component updates as it should due to changes in an input element. I use the fireEvent.change()-function, and if I then check the value of the node I found using getByPlaceholderText it has updated as it should. However I cannot see the changes in the react component itself.
This might be because the changes don't happen until a rerender; how would I test this? react-testing-library's rerender appears to start the component "from scratch" (i.e. without the new input value), and waitForElement never finds what it's waiting for.
Here's the component TestForm.js:
import React from 'react';
import { withState } from 'recompose';
const initialInputValue = 'initialInputValue';
const TestForm = ({ inputValue, setInputValue }) => (
<>
{console.log('inputValue', inputValue)}
<input value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="placeholder" />
{inputValue !== initialInputValue && <div>Input has changed</div>}
</>
);
export default withState('inputValue', 'setInputValue', initialInputValue)(TestForm);
And here's the test, run using npx jest test.js:
import React from 'react';
import { cleanup, fireEvent, render, waitForElement } from 'react-testing-library';
import TestForm from './TestForm';
afterEach(cleanup);
describe('TestForm', () => {
it('Change input', async () => {
const { getByPlaceholderText, getByText } = render(<TestForm />);
const inputNode = getByPlaceholderText('placeholder');
fireEvent.change(inputNode, { target: { value: 'new value' } });
console.log('inputNode.value', inputNode.value);
await waitForElement(() => getByText('Input has changed'));
});
});
This code works for me:
import React from "react";
const initialInputValue = "initialInputValue";
class TestForm extends React.Component {
constructor(props) {
super(props);
this.state = { inputValue: initialInputValue };
}
render() {
const { inputValue } = this.state;
return (
<div>
{console.log("inputValue", inputValue)}
<input
value={inputValue}
onChange={e => this.setState({ inputValue: e.target.value })}
placeholder="placeholder"
/>
{inputValue !== initialInputValue && <div>Input has changed</div>}
</div>
);
}
}
import { render, cleanup, fireEvent } from "react-testing-library";
import "jest-dom/extend-expect";
afterEach(cleanup);
test("form", () => {
const { getByPlaceholderText, getByText } = render(<TestForm />);
fireEvent.change(getByPlaceholderText("placeholder"), {
target: { value: "new value" }
});
expect(getByText("Input has changed")).toBeInTheDocument();
});
It does not work in codesandbox though, I guess they have some issues with keeping the browser and the test environment separated.
using user-event library
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
afterEach(cleanup);
test("form", async () => {
const user = userEvent.setup();
const { getByPlaceholderText, getByText } = render(<TestForm />);
await user.type(getByPlaceholderText("placeholder"), "new value");
await waitFor(() => {
expect(getByText("Input has changed")).toBeInTheDocument();
});
});
Related
I'm currently making a simple web frontend with react using react-autosuggest to search a specified user from a list. I want to try and use the Autosuggest to give suggestion when the user's type in the query in the search field; the suggestion will be based on username of github profiles taken from github user API.
What I want to do is to separate the AutoSuggest.jsx and then import it into Main.jsx then render the Main.jsx in App.js, however it keeps giving me 'TypeError: _ref2 is undefined' and always refer to my onChange function of AutoSuggest.jsx as the problem.
Below is my App.js code:
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import Header from './views/header/Header';
import Main from './views/main/Main';
import Footer from './views/footer/Footer';
const App = () => {
return (
<>
<Header/>
<Main/> <- the autosuggest is imported in here
<Footer/>
</>
);
}
export default App;
Below is my Main.jsx code:
import React, { useState } from 'react';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import axios from 'axios';
import { useEffect } from 'react';
import AutoSuggest from '../../components/AutoSuggest';
const Main = () => {
const [userList, setUserList] = useState([]);
useEffect(() => {
axios.get('https://api.github.com/users?per_page=100')
.then((res) => setUserList(res.data))
.catch((err) => console.log(err));
}, [])
return (
<Container>
<br/>
<Row>
<AutoSuggest userList={userList} placeHolderText={'wow'} />
</Row>
</Container>
);
}
export default Main;
Below is my AutoSuggest.jsx code:
import React, { useState } from "react";
import Autosuggest from 'react-autosuggest';
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getSuggestions(value, userList) {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp('^' + escapedValue, 'i');
return userList.filter(user => regex.test(user.login));
}
function getSuggestionValue(suggestion) {
return suggestion.name;
}
function renderSuggestion(suggestion) {
return (
<span>{suggestion.name}</span>
);
}
const AutoSuggest = ({userList, placeHolderText}) => {
const [value, setValue] = useState('');
const [suggestions, setSuggestions] = useState([]);
const onChange = (event, { newValue, method }) => { <- error from console always refer here, I'm not quite sure how to handle it..
setValue(newValue);
};
const onSuggestionsFetchRequested = ({ value }) => {
setValue(getSuggestions(value, userList))
};
const onSuggestionsClearRequested = () => {
setSuggestions([]);
};
const inputProps = {
placeholder: placeHolderText,
value,
onChange: () => onChange()
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={() => onSuggestionsFetchRequested()}
onSuggestionsClearRequested={() => onSuggestionsClearRequested()}
getSuggestionValue={() => getSuggestionValue()}
renderSuggestion={() => renderSuggestion()}
inputProps={inputProps} />
);
}
export default AutoSuggest;
The error on browser (Firefox) console:
I have no idea what does the error mean or how it happened and therefore unable to do any workaround.. I also want to ask if what I do here is already considered a good practice or not and maybe some inputs on what I can improve as well to make my code cleaner and web faster. Any input is highly appreciated, thank you in advance!
you have to write it like this... do not use the arrow function in inputProps
onChange: onChange
I have the following react code in my project
import React from 'react';
import { Upload } from 'antd';
const { Dragger } = Upload;
...
<Dragger
accept={ACCEPTED_FORMATS}
beforeUpload={beforeUpload}
data-testid="upload-dragger"
maxCount={1}
onChange={({ file: { status } }) => {
if (status === 'done') onUploadComplete();
}}
progress={progress}
showUploadList={false}
>
{/* here i have a button, code ommited for clarity, if needed i'll post it */}
</Dragger>
And I want to test if the callback function onUploadComplete() was called when file.status is 'done'.
Here is how i am doing the test right now:
I have a jest.mock to simulate a dumb request that will always succeed
import React from 'react';
import { fireEvent, render, waitFor } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import { DraggerProps } from 'antd/lib/upload';
import userEvent from '#testing-library/user-event';
import UploadCompanyLogo from '../UploadCompanyLogo'; // This is the component where the dragger is placed
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
console.log('log test');
return (
<Dragger
{...props}
action="greetings"
customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0);
}}
/>
);
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
The test itself (same file as the mock), where it renders the component (where antd will be imported), simulate an upload and then checks if the callback has been called.
it('completes the image upload', async () => {
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(elementRenderer({ onUploadComplete }));
const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
const uploadDragger = await waitFor(() => getByTestId('upload-dragger'));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalledTimes(1);
expect(onUploadComplete).toHaveBeenCalledWith();
});
elementRenderer
const elementRenderer = ({
onUploadComplete = () => {}
}) => (
<ApplicationProvider>
<UploadCompanyLogo
onUploadComplete={onUploadComplete}
/>
</ApplicationProvider>
);
ApplicationProvider
import React, { PropsWithChildren } from 'react';
import { ConfigProvider as AntdProvider } from 'antd';
import { RendererProvider as FelaProvider } from 'react-fela';
import { createRenderer } from 'fela';
import { I18nextProvider } from 'react-i18next';
import antdExternalContainer, {
EXTERNAL_CONTAINER_ID,
} from 'src/util/antdExternalContainer';
import { antdLocale } from 'src/util/locales';
import rendererConfig from 'src/fela/felaConfig';
import i18n from 'src/i18n';
const ApplicationProvider = (props: PropsWithChildren<{}>) => {
const { children } = props;
return (
<AntdProvider locale={antdLocale} getPopupContainer={antdExternalContainer}>
<FelaProvider renderer={createRenderer(rendererConfig)}>
<I18nextProvider i18n={i18n}>
<div className="antd-local">
<div id={EXTERNAL_CONTAINER_ID} />
{children}
</div>
</I18nextProvider>
</FelaProvider>
</AntdProvider>
);
};
export default ApplicationProvider;
This is currently not working, but have already been improved with the help of #diedu.
The console.log() I have put in the MockedDragger it's currently not showing. If I put a console.log() in both component and mockedDragger, it prints the component log.
Any tips on how to proceed?
I have already seen this issue and didn’t help.
The first thing you should change is the return you are doing in your mock. You are returning a new component called MockedDragger, not Dragger.
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
The next thing is the event firing. According to this issue you should use RTL user-event library and wrap the call in act
import userEvent from "#testing-library/user-event";
...
await act(async () => {
userEvent.upload(uploadDragger, file);
});
...
and finally, due to some asynchronous code running you need to flush the pending promises before checking the call
const flushPromises = () => new Promise(setImmediate);
...
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled()
this is the full version
import { render, waitFor } from "#testing-library/react";
import App from "./App";
import userEvent from "#testing-library/user-event";
import { act } from "react-dom/test-utils";
import { DraggerProps } from "antd/lib/upload";
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const { Upload } = antd;
const { Dragger } = Upload;
const MockedDragger = (props: DraggerProps) => {
return <Dragger {...props} customRequest={({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 0)
}} />;
};
return { ...antd, Upload: { ...Upload, Dragger: MockedDragger } };
});
it("completes the image upload", async () => {
const onUploadComplete = jest.fn();
const flushPromises = () => new Promise(setImmediate);
const { getByTestId } = render(
<App onUploadComplete={onUploadComplete} />
);
const file = new File(["(⌐□_□)"], "chucknorris.png", { type: "image/png" });
const uploadDragger = await waitFor(() => getByTestId("upload-dragger"));
await act(async () => {
userEvent.upload(uploadDragger, file);
});
await flushPromises();
expect(onUploadComplete).toHaveBeenCalled();
});
Unfortunately, I couldn't set up a codesandbox to show it's working, but let me know if you face any issue and I can push the code to a repo.
I am new to Jest and Enzyme & facing an issue. Can someone help?
Here is my jsx file (myTemplate.jsx) :
export default (props) => {
const oneSection = () => {
return <div>Hello</div>
};
return (
props.hasData ? { <div>Hey</div> } : { <div><oneSection/></div> }
)
}
And this is my test file :
import React from "react";
import { shallow } from "enzyme";
import myTemplate from '../myTemplate';
describe("template component", () => {
const props= {
hasData: false,
}
let myComponent = null;
beforeAll(() => {
myComponent = shallow(<myTemplate {...props}/>);
})
test("should render with initial state properly", () => {
expect(myComponent).toMatchSnapshot();
})
})
Now this test case is running successfully. Snapshot is getting created with a div which has oneSection but oneSection is not getting replaced with actual html within it. Basically these lines are not getting covered :
const oneSection = () => {
return <div>Hello</div>
};
How can i cover this piece of code using Jest and enzyme ?
import React from 'react'
import { cleanup, render } from '#testing-library/react'
describe('DMReports', () => {
afterEach(cleanup)
test('DMReports: hasData true', () => {
const { container } = render(
<DMReports hasData={true}/>,
)
expect(container).toMatchSnapshot()
})
test('DMReports: hasData false', () => {
const { container, getByText } = render(
<DMReports hasData={false}/>,
)
expect(container).toMatchSnapshot()
expect(getByText('Hello')).toBeTruthy()
})
})
Currently have an issue where by I want to update props based on 'componentdidupdate'. However everytime i call this function (onUpdateSelectedDate), it keeps saying
onUpdateSelectedDate is not defined
I have tried the following:
onUpdateSelectedDate(toggledDate)
this.onUpdateSelectedDate(toggledDate)
this.props.onUpdateSelectedDate(toggledDate)
and still unsure why i am getting this error.
Code below
import DayPicker from "react-day-picker"
import React, {Component} from 'react'
import './calendarDatePicker.scss'
import propTypes from 'prop-types'
import { connect } from 'react-redux'
class CalendarDatePicker extends Component {
state = {
toggledDate: null,
}
componentDidUpdate = () => {
const toggledDate = this.state.toggledDate
onUpdateSelectedDate(toggledDate)
}
render() {
const selectedDate = this.props.selectedDays
const onDayClick = this.props.onDayClick
const toggledDate = this.state.toggledDate
const modifiers = {
}
return (
<DayPicker
selectedDays={toggledDate===null ? selectedDate : toggledDate}
onDayClick={onDayClick}
todayButton="Go to Today"
firstDayOfWeek={1}
modifiers = {modifiers}
onMonthChange={(d) => this.setState({toggledDate: d})}
/>
)
}
}
CalendarDatePicker.propTypes = {
selectedDays: propTypes.instanceOf(Date),
onDayClick: propTypes.func,
onUpdateSelectedDate: propTypes.func,
}
const mapStateToProps = (state) => {
return {
//toggledDate: state.diaryContext.activities.selectedDates,
}
}
const mapDispatchToProps = (dispatch) => {
return {
onUpdateSelectedDate: (toggledDate) => { dispatch(diaryActions.updateSelectedDate(toggledDate)) },
}
}
export default connect(null, mapDispatchToProps)(CalendarDatePicker)
You use a wrong signature for the componentDidUpdate method it should be componentDidUpdate(prevProps, prevState) and then you can access your function from mapStateToProps like that:
componentDidUpdate (prevProps, prevState) {
const toggledDate = prevState.toggledDate
prevProps.onUpdateSelectedDate(toggledDate)
}
I've tried to fire an onchange function when my Textfield is filled, but i can't figure out why this function is never fired, even if React devtool plugin for Chrome actually trigger the changes, any advice ?
import React, {Component} from 'react';
import {Tracker} from 'meteor/tracker';
import {Meteor} from 'meteor/meteor';
import {Links} from '../api/links';
import LinkListItem from './LinkListItem';
import {Session} from 'meteor/session';
import SearchLink from './SearchLink';
import Fuse from 'fuse.js';
export default class LinkList extends Component {
constructor(props) {
super(props);
this.state = {
links: [],
inputValue: ''
};
}
componentDidMount() {
this.linksTracker = Tracker.autorun(() => {
Meteor.subscribe('links');
const links = Links.find({visible:
Session.get('showVisible')}).fetch();
this.setState({links});
});
}
componentWillUnmount() {
this.linksTracker.stop();
}
renderLinksListItems() {
if (this.state.links.length === 0) {
return (
<div>
<h2 className="link">{Session.get('showVisible') ? 'No links found' : 'No hidden links found'}</h2>
</div>
);
}
console.log(this.state.links);
return this.state.links.map((link) => {
const shortUrl = Meteor.absoluteUrl(link._id);
return <LinkListItem key={link._id} shortUrl={shortUrl} {...link}/>;
});
}
_onChange(e) {
if(e.target.value === "") {
return;
}
var fuse = new Fuse(this.state.links, { keys: ["url"]});
var result = fuse.search(e.target.value);
this.setState({
inputValue: e.target.value,
links: result
});
}
render() {
return (
<div>
<div>
<SearchLink onChange={this._onChange} value={this.state.inputValue}/>
</div>
<div>{this.renderLinksListItems()}</div>
</div>
);
}
}
My Textfield component :
import React from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import TextField from 'material-ui/TextField';
const muiTheme = getMuiTheme({
palette: {
primary1Color: '#ef6c00'
}
})
const SearchLink = () => (
<MuiThemeProvider muiTheme={muiTheme}>
<TextField floatingLabelText="Search a Link" name="searchLink" fullWidth={true}/>
</MuiThemeProvider>
);
export default SearchLink;
Thank you for your help!
Do these changes:
1. Bind the method in Parent component LinkList, because you are using this.setState inside onChange method, if you don't bind it, it will throw the error, bind it like this:
<SearchLink onChange={this._onChange.bind(this)} value={this.state.inputValue}/>
or define the binding in constructor.
2. You are passing the event and value in props, so you need to define those values in TextField, like this:
const SearchLink = (props) => (
<MuiThemeProvider muiTheme={muiTheme}>
<TextField
onChange = {props.onChange}
value = {props.value}
floatingLabelText = "Search a Link"
name = "searchLink"
fullWidth = {true}/>
</MuiThemeProvider>
);