enzyme
enzyme copied to clipboard
wrapper.instance() is null on class component
Current behavior
wrapper.instance() and wrapper.state() are not working on a class component.
Expected behavior
wrapper.instance() and wrapper.state() work on a class component.
Your environment
I'm really confused by a component not working with instances. I have many other similar components that have a the same structure that are working. I have broken this down to a very simple component and test to show the problem.
React Component
import React from 'react';
export class EnzymeTest extends React.Component {
constructor(props) {
super(props);
this.state = {
test: true
}
}
componentDidMount() {
this.setState({test: false})
}
render() {
if (this.state.test) return <div>Test is True</div>
else return <div>Test is False</div>
}
};
Enzyme Test
import React from 'react';
import { shallow } from 'enzyme';
import { EnzymeTest } from './EnzymeTest.js';
let wrapper;
describe('isOutsideDateRange', () => {
beforeEach(() => {
wrapper = shallow(<EnzymeTest /> )
});
beforeEach(() => {
wrapper.unmount();
});
test('has state', () => {
expect(wrapper.state('test')).toBe('blah');
});
test('has a wrapper instance', () => {
expect(wrapper.instance()).toBeTruthy();
});
});
Both of the above tests fail. The state test shows this error: ShallowWrapper::state() can only be called on class components
and wrapper.instance() in null.
This is a stateful class component so I'm confused about this behavior.
API
- [ x] shallow
- [ ] mount
- [ ] render
Version
library | version |
---|---|
enzyme | 3.3 |
react | 16.3.2 |
react-dom | 16.3.2 |
react-test-renderer | 16.3.2 |
adapter (below) |
Adapter
- [x ] enzyme-adapter-react-16
- [ ] enzyme-adapter-react-16.3
- [ ] enzyme-adapter-react-16.2
- [ ] enzyme-adapter-react-16.1
- [ ] enzyme-adapter-react-15
- [ ] enzyme-adapter-react-15.4
- [ ] enzyme-adapter-react-14
- [ ] enzyme-adapter-react-13
- [ ] enzyme-adapter-react-helper
- [ ] others ( )
In this case, wrapper isn’t an EnzymeTest, it’s whatever EnzymeTest renders. You can verify this with wrapper.debug()
.
I'm not trying to test against wrapper.instance(). However, the actual component that I'm trying to test has class methods that I want to unit test, so I want to unit test them via wrapper.instance().someMethod()... bu t I can't because wrapper.instance() returns null.
What am I doing wrong? How can I run tests against those class methods? I'm doing the same type of testing in plenty of other components so I'm confused why instance() is null in this case.
Can you share the actual unmodified component and test code? “Simplified” examples rarely simplify things, unfortunately.
React Component
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { fetchAirports } from '~/services/airports';
import DayPicker from '../DayPicker';
import DropDown from '../DropDown';
import SearchInput from '../SearchInput';
/** Controlled form that allows users to input the necessary info to search for flights */
export class FlightPicker extends Component {
constructor(props) {
super(props);
this.options = [
{ key: 0, text: 'Any Time', value: 0 },
{ key: 8, text: 'Morning 6AM-10AM', value: 8 },
{ key: 9, text: 'Midday 10AM-2PM', value: 9 },
{ key: 10, text: 'Afternoon 3PM-7PM', value: 10 },
{ key: 11, text: 'Evening 8PM-12AM', value: 11 },
{ key: 1, text: '5AM-7AM', value: 1 },
{ key: 2, text: '7AM-9AM', value: 2 },
{ key: 3, text: '9AM-12PM', value: 3 },
{ key: 4, text: '12PM-3PM', value: 4 },
{ key: 5, text: '3PM-5PM', value: 5 },
{ key: 6, text: '5PM-7PM', value: 6 },
{ key: 7, text: '7PM-11PM', value: 7 },
];
this.mapAirport = this.mapAirport.bind(this);
this.mapAirports = this.mapAirports.bind(this);
}
isOutsideDateRange(date) {
return date.isBefore(this.props.outboundDate);
}
setOutboundDate = (date) => {
date && date.isBefore(this.props.returnDate) ? this.props.onChange({outboundDate: date}) : this.props.onChange({outboundDate: date, returnDate: date})
}
mapAirport(airport) {
return {
title: `${airport.city}, ${airport.countryName} (${airport.iata})`,
iata: airport.iata,
name: airport.name
}
}
mapAirports(airports) {
// Map our airport code matches
const codeMatches = airports.iataCodeMatches.map(this.mapAirport);
// Put airport code matches in an object to easily filter them out of the name results
const codeMatchesObj = {};
codeMatches.forEach(el => codeMatchesObj[el.iata] = true);
return codeMatches.concat(airports.cityNameMatches.filter(city => city.iata && !codeMatchesObj[city.iata]).map(this.mapAirport));
}
resultRenderer = ({title, name}) => (
<div>
<p style={{fontSize: '14px', margin: '4px 0', fontWeight: '600'}}>{title}</p>
<p style={{fontSize: '10px', margin: '4px 0', color: 'gray'}}>{name}</p>
</div>
);
searchAirports(searchString) {
return fetchAirports(searchString.toLowerCase());
}
render() {
const {
fromAirport,
outboundDate,
outboundTimeSlotId,
toAirport,
returnDate,
returnTimeSlotId,
isRoundTrip,
onChange
} = this.props;
return (
<div className="flight-picker component">
<div className="from-airport-picker">
<span className="form-label">Flying From</span>
<SearchInput
className='from-airport'
defaultValue={fromAirport ? fromAirport.title : ''}
handleSearch={this.searchAirports}
mapResults={this.mapAirports}
placeholder="City or Airport"
reset={() => onChange({fromAirport: ''})}
resultRenderer={this.resultRenderer}
setSelection={(airport) => onChange({fromAirport: airport})} />
</div>
<div className="to-airport-picker">
<span className="form-label">Flying To</span>
<SearchInput
className='to-airport'
defaultValue={toAirport ? toAirport.title : ''}
handleSearch={this.searchAirports}
mapResults={this.mapAirports}
placeholder="City or Airport"
reset={() => onChange({toAirport: ''})}
resultRenderer={this.resultRenderer}
setSelection={(airport) => onChange({toAirport: airport})} />
</div>
<div className='outbound'>
<span className="form-label">Departing</span>
<div className='outbound-date-time'>
<div className='date-picker'>
<DayPicker
date={outboundDate}
onDateChange={date => this.setOutboundDate(date)}
/>
</div>
<div className='time-picker'>
<DropDown
centered
leftIcon='icon-time'
value={outboundTimeSlotId}
onChange={(value) => onChange({outboundTimeSlotId: value})}
options={this.options}
/>
</div>
</div>
</div>
{!isRoundTrip ? null : (
<div className='return'>
<span className="form-label">Returning</span>
<div className='return-date-time'>
<div className='date-picker'>
<DayPicker
date={returnDate}
disabled={!outboundDate}
isOutsideRange={(day) => this.isOutsideDateRange(day)}
onDateChange={date => onChange({returnDate: date})}
/>
</div>
<div className='time-picker'>
<DropDown
centered
leftIcon='icon-time'
value={returnTimeSlotId}
onChange={(value) => onChange({returnTimeSlotId: value})}
options={this.options}
/>
</div>
</div>
</div> ) }
</div>
);
}
}
FlightPicker.propTypes = {
/** Data for the origin airport */
fromAirport: PropTypes.object,
/** Moment object for day of outbound flight */
outboundDate: PropTypes.object,
/** User selection for timeslot of outbound flight */
outboundTimeSlotId: PropTypes.number,
/** Data for the destination airport */
toAirport: PropTypes.object,
/** Moment object for day of return flight */
returnDate: PropTypes.object,
/** User selection for timeslot of return flight */
returnTimeSlotId: PropTypes.number,
/** Toggle for whether we are searching round trip or one way */
isRoundTrip: PropTypes.bool.isRequired,
/** Handles all changes from the flightpicker */
onChange: PropTypes.func.isRequired
};
export default FlightPicker;
Test File
import React from 'react';
import { shallow } from 'enzyme';
import { FlightPicker } from './index.js';
import moment from 'moment';
let wrapper;
const onChange = jest.fn();
describe('basic render Check', () => {
beforeEach(() => {
wrapper = shallow(<FlightPicker onChange={onChange} isRoundTrip={true} /> )
});
beforeEach(() => {
wrapper.unmount();
});
test('it renders a flight picker div', () => {
expect(wrapper.find('.flight-picker').length).toBe(1); // This passes which indicates the wrapper is able to render
});
});
describe('isOutsideDateRange', () => {
beforeEach(() => {
const today = moment();
wrapper = shallow(<FlightPicker onChange={onChange} isRoundTrip={true} outboundDate={today}/> )
});
beforeEach(() => {
wrapper.unmount();
});
test('returns false if the argument is after the outboundDate prop', () => {
const tomorrow = moment().add(1, 'days');
const result = wrapper.instance().isOutsideDateRange(tomorrow); // This line throws TypeError: Cannot read property 'isOutsideDateRange' of null
expect(result).toBe(false);
})
});
In this case, it seems like you're trying to unit-test the isOutsideDateRange
method - can you do:
test('returns false if the argument is after the outboundDate prop', () => {
const tomorrow = moment().add(1, 'days');
const result = FlightPicker.prototype.isOutsideDateRange.call({ props: { outboundDate: tomorrow } });
expect(result).toBe(false);
});
Alternatively, if you want to test the react render tree, I'd suggest this:
test('returns false if the argument is after the outboundDate prop', () => {
const tomorrow = moment().add(1, 'days');
const result = wrapper.find(DayPicker).prop('isOutsideDateRange')(tomorrow);
expect(result).toBe(false);
});
The latter one means you're not coupling your tests to the implementation details of FlightPicker
- instead, you're testing the thing you actually care about, which is that the callback passed to DayPicker
behaves properly.
Thanks for the prompt response. The latter approach seems like a good one, and it did work out for me with some slight syntax modifications.
I am still curious why wrapper.instance() is returning null for this component though, so if you have any insight on that I would greatly appreciate it.
Again, it’s because the shallow wrapper isn’t a FlightPicker, it’s a div (specifically, the root div that FlightPicker renders), and html elements don’t have an instance.
Shallow wrappers are what the component renders - mount wrappers, on the other hand, are the component itself.
I guess my point of confusion is that I'm using .instance() on shallow wrappers for other components with no issue, and when I do wrapper.debug() on those tests, I see that the wrapper is the div that the component renders. So, I have no idea why wrapper.instance() is working on shallow wrappers in some cases but not this one.
Can you share the component and test code for one of the ones that works? Note that if the other ones have an HOC, then wrapper
would be the wrapped component instead of the wrapper.
Here's a simple example.
import React, {Component} from 'react';
import {Image} from 'semantic-ui-react';
import {fetchData} from '~/services/airlines';
import PropTypes from 'prop-types'
/**
* Airline card for flight itineraries.
*/
export class Airline extends Component {
static defaultProps = {
width: 25
}
state = {}
componentDidMount() {
this.fetchAirline();
}
componentDidUpdate(prevProps) {
if (prevProps.code !== this.props.code) {
this.setState({airline: null});
this.fetchAirline();
}
}
fetchAirline() {
const { code, width } = this.props;
fetchData({"airlinecode": code, width})
.then(airline => this.setState({airline}))
.catch(() => console.log('error fetching airline'));
}
render() {
const { showCode, showName } = this.props;
const { airline } = this.state;
if(airline)
return (
<div className="airline snippet">
<Image src={airline.secondaryImage} title={airline.name} verticalAlign="bottom" inline/>
{showCode && <span> {airline.code}</span>}
{showName && <span> {airline.name}</span>}
</div>
);
return null;
}
}
Airline.propTypes = {
/** Airline code used for logo lookup */
code: PropTypes.string.isRequired,
/** Determines whether or not we show the airline code */
showCode: PropTypes.bool,
/** Determines whether or not we show the airline name */
showName: PropTypes.bool,
/** Determines the width of the airline logo. */
width: PropTypes.number
}
export default Airline;
import React from 'react';
import Airline from './index';
import { shallow } from 'enzyme';
import * as services from '~/services/airlines';
let oldFetchData, wrapper;
describe('fetchAirline', () => {
beforeEach(() => {
oldFetchData = services.fetchData;
wrapper = shallow(<Airline code='AA' />);
wrapper.setState({airline: 'originalAirline'});
services.fetchData = jest.fn(() => Promise.resolve('test'));
});
afterEach(() => {
services.fetchData = oldFetchData;
wrapper.unmount();
});
test('fetchAirline calls fetchData with the expected arguments', () => {
expect(services.fetchData.mock.calls.length).toBe(0);
wrapper.instance().fetchAirline();
expect(services.fetchData.mock.calls.length).toBe(1);
expect(services.fetchData.mock.calls[0][0].airlinecode).toBe('AA');
expect(services.fetchData.mock.calls[0][0].width).toBeTruthy();
});
test('fetchAirline sets state to the airline returned by fetchData', async () => {
expect(wrapper.state('airline')).toBe('originalAirline');
await wrapper.instance().fetchAirline();
expect(wrapper.state('airline')).toBe('test');
});
});
and which test line doesn’t work?
i see a problem (maybe not the one for this issue tho). When you call fetchAirline, that fires off a promise chain. Your test completes many thousands of ms before the setState is called.
You have to wait on the same promises that are inside fetchAirline - in this case, if you make it return the promise, then you can do:
import React from 'react';
import Airline from './index';
import { shallow } from 'enzyme';
import * as services from '~/services/airlines';
let oldFetchData, wrapper;
describe('fetchAirline', () => {
beforeEach(() => {
oldFetchData = services.fetchData;
wrapper = shallow(<Airline code='AA' />);
wrapper.setState({airline: 'originalAirline'});
services.fetchData = jest.fn(() => Promise.resolve('test'));
});
afterEach(() => {
services.fetchData = oldFetchData;
wrapper.unmount();
});
test('fetchAirline calls fetchData with the expected arguments', async () => {
expect(services.fetchData.mock.calls.length).toBe(0);
await wrapper.instance().fetchAirline();
expect(services.fetchData.mock.calls.length).toBe(1);
expect(services.fetchData.mock.calls[0][0].airlinecode).toBe('AA');
expect(services.fetchData.mock.calls[0][0].width).toBeTruthy();
});
test('fetchAirline sets state to the airline returned by fetchData', async () => {
expect(wrapper.state('airline')).toBe('originalAirline');
await wrapper.instance().fetchAirline();
expect(wrapper.state('airline')).toBe('test');
});
});
(note how the first one wasn’t awaiting; and your actual impl isn’t returning the promise, only the mock is)
Please check out this codesandbox instance. I am getting null for an instance created by a class component. https://codesandbox.io/s/dreamy-tesla-3rktt I am getting null when calling instance() on it
export default class Tab extends React.Component {
constructor(props) {
super(props);
this.myRef = createRef();
}
render() {
return (
<div className="tab_bar">
<ul
ref={this.myRef}
className="tab__list direction--row"
role="tablist"
>
<li>TEST</li>
</ul>
</div>
);
}
}
describe("Tab component", () => {
it("should render", () => {
const wrapper = mount(<Tab />);
const ulElement = wrapper.find("ul");
expect(ulElement.instance()).not.toBeNull();
expect(wrapper.instance()).not.toBeNull();
});
});
@jdc91 tests are passing for me in that sandbox. what should i be looking for?
Sorry check again!
The ul
shouldn't necessarily have an instance; but the fact that wrapper.instance()
seems to throw is definitely a problem.
However, your sandbox is using the 16.3 adapter with React 16.8.
if you upgrade the adapter: https://codesandbox.io/s/enzyme-instance-wrvig?fontsize=14 or downgrade React: https://codesandbox.io/s/enzyme-instance-uxn18?fontsize=14
everything passes.
Remember, when npm ls
exits nonzero - in any node project - your dep graph is invalid, and you must not rely on anything working.
Thanks @ljharb :)
In case it helps anyone, I was experiencing a similar problem; in my case, the issue was triggered by updating react-redux to v6 or higher, and seems to have been resolved by adding an extra .dive()
to shallow wrappers on problematic tests.