enzyme: ShallowWrapper instance of Stateless React component returns null

Hello,

After update to enzyme 3 and React 16, ShallowWrapper instance() works a bit different. For Stateless React component it returns null. Please check test example bellow:

import { shallow } from 'enzyme';
import React from 'react';

function Stateless({ name }) {
	return <div>{name}</div>;
}

class Statefull extends React.Component {
	render() {
		return <div>{this.props.name}</div>;
	}
}

describe('enzyme 3 with React 16', () => {
	const props = {
		name: 'Test',
	};

	describe('Stateless component', () => {
		test('shallow wrapper instance should not be null', () => {
			const wrapper = shallow(<Stateless {...props} />);
			const instance = wrapper.instance();

		    expect(instance).not.toBe(null);
		});
	});

	describe('Statefull component', () => {
		test('shallow wrapper instance should not be null', () => {
			const wrapper = shallow(<Statefull {...props} />);
			const instance = wrapper.instance();

		    expect(instance).not.toBe(null);
		});
	});
});

As a result I have an error in next test enzyme with React 16 › Stateless component › shallow wrapper instance should not be null. Also, these tests are passed with enzyme 3 + enzyme-adapter-react-15 + React 15.

setup

const Enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-16');

Enzyme.configure({ adapter: new Adapter() });

package.json

{
    ...
    "dependencies": {
      ...
      "react": "^16.0.0",
      "react-dom": "^16.0.0",
      ...
    },
    "devDependencies": {
      ...
      "enzyme": "^3.1.0",
      "enzyme-adapter-react-16": "^1.0.1",
      "jest": "^20.0.4",
      "react-test-renderer": "^16.0.0",
      ...
    },
    ...
}

So, is it configuration issue, or something else?

Thanks for any help.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 18
  • Comments: 24 (13 by maintainers)

Most upvoted comments

For people coming to this from a search engine, if the wrapper.props() solution above doesn’t work for you, try wrapper.getElement().props—it worked for me.

@mrchief : I have been in the similar scenario except that it was for react-native. wrapper.props() in the DatePicker example is going to show props of the top level component (in your case <div>)

Note that a shallow wrapper is what the component renders, not the component itself

If you follow this, the shallowWrapper is nothing but the top level component (atleast for SFC, thats what I have deduced so far)

I would suggest doing something like this:

let mockHandleChange = jest.fn()
const wrapper = shallow(<DatePickerField {...props} handleChange={mockHandleChange} />)

wrapper.find('DatePicker').getElemet().props.onChange()

expect(mockHandleChange).toBeCalled()

Why would someone want to write such a test? I write to test the wiring of callbacks/handlers

Hope this helps

@ljharb In this examples it’s just a few lines above, but I have cases where test preconditions are a bit higher. And it’s easy to read expects if everything is in place. But thank you for wrapper.props().

And probably NOTE section in this doc is valid only for Statefull React Component in case of React 16. Because, as I showed in example above, wrapper.instance().props might work a bit unexpected (wrapper.instance() // null) with Stateless React Component (React 16). What do you think?

In case if you need some help, I can create PR with updated doc if needed.

@mrchief that all seems correct to me. Your second SFC renders an element with a onPageChange prop. Your first SFC, however, renders a div with no props. Note that a shallow wrapper is what the component renders, not the component itself - separately, it makes no sense to assert on props you literally just passed in.

wrapper.find(Something).props() works too. .instance().props() - which was always redundant - is the one that doesn’t work on SFCs in React 16.

This seems answered.

wrapper.props() or wrapper.getElement().props (which points to same object as wrapper.props) has some funkiness going on though.

E.g., with the following SFC,

export const DatePickerField = ({ input, meta: { dirty, error } }: DatePickerProps) => {
  const format = 'MM/DD/YYYY hh:mm A'

  const handleChange = date => {
    input.onChange(date ? date.toISOString() : null)
  }

  return (
    <div className={cx('form-group')}>
      <label>Schedule for processing at a future date <br />
        All times are in Eastern Standard/Daylight Time (USA-New York).
      </label>
      <div>
        <DatePicker
          isClearable
          minDate={moment()}
          onChange={handleChange}
          placeholderText="Click to select a date"
          selected={input.value ? moment(input.value) : null}
          todayButton="Today"
          showTimeSelect
          timeFormat="HH:mm"
          timeIntervals={15}
          dateFormat={format}
        />
        {dirty && error && <span>{error}</span>}
      </div>
    </div>
  )
}

// test.js
const wrapper = shallow(<DatePickerField {...props} />)
wrapper.props().handleChange()

I get wrapper.props(...).handleChange is not a function error.

However, with the following SFC:

export const JobList = (props: Props) => {
  const { data: { pages, jobs, page, pageSize }, onPagination, onSort, onJobClick, onEditClick, onDeleteClick } = props
  const onPageChange = index => onPagination({ page: index })

  const onPageSizeChange = size => {
    // let's pretend there is good reason for doing this
    onPagination({ page: 0, pageSize: size })
  }

  const onSortedChange = ([{ id, desc }]) => onSort(id, desc)

  return (
    <ReactTable
      manual
      multiSort={false}
      showPaginationTop
      showPageJump={false}
      minRows={0}
      data={jobs}
      columns={getColumns(onJobClick, onEditClick, onDeleteClick)}
      pageSize={pageSize}
      page={page}
      pages={pages}
      onPageChange={onPageChange}
      onPageSizeChange={onPageSizeChange}
      onSortedChange={onSortedChange}
      defaultSorted={[{ id: 'id', desc: false }]}
      className="table striped-even"
    />
  )
}

// test.js
const wrapper = shallow(<JobList {...props} />)
wrapper.props().onPageChange(2)

seems to work fine without any issues.

I was using instance.props to test the default values of props.

let wrapper;
let instance;

before(() => {
   wrapper = shallow(<SomeComponent />);
   instance = wrapper.instance();
});

describe('default props', () => {
   it('should default isVisible to true', () => {
      assert.isTrue(instance.props.isVisible);
   });

   it('should default className to null', () => {
      assert.isNull(instance.props.className);
   });
});

Checking wrapper.props() works except for the case where the incoming prop is used to modify a prop on the rendered component of the same name. For example, I’ve used className as a prop that can be used to apply an additional class to the className of the root element:

const SomeComponent = ({ children = null, className = null}) => (
   <div
      className={classNames(classes.root, className)}
   >
      {children}
   </div>
);

If I’m expecting the className prop to be null by default, asserting that wrapper.props().className is null will fail because its value will include the classes applied within the component itself.

I suppose I can check the number of classes applied to the root element, but it was nice to be able to test the values received by the functional component.

UPDATED:

…or I could set my default props like this and write tests against SomeComponent.defaultProps:

SomeComponent.defaultProps = { children: null, className: null };

…but only if I’m not exporting the product of a HoC. In that case, I’d need to export the component for testing and the result of the HoC for use in the application.

Is there a better way of testing default props?

Final update:

@ljharb answered my question a while ago when he wrote:

…you should be testing the effect of your defaultProps, not the literal value of those props. TDD is about documenting the effect of your code - not its internal implementation, which is what defaultProps are.

Fair enough.