Package detail

unexpected-react

bruderstein1.7kMIT6.0.2

Plugin for unexpected, to allow for assertions on the React.js virtual DOM, and the shallow and test renderers

unexpected-plugin, unexpected, testing, react

readme

Build Status Coverage Status npm version

unexpected-react

Plugin for unexpected to allow for testing the full virtual DOM, and against the shallow renderer (replaces unexpected-react-shallow)

!output_demo

See the blog post for an introduction: https://medium.com/@bruderstein/the-missing-piece-to-the-react-testing-puzzle-c51cd30df7a0

Documentation

The full documentation with all the assertions: http://bruderstein.github.io/unexpected-react

Important note about React v16 support

Note that in a mocha jsdom environment you also need a polyfill for requestAnimationFrame

Features

  • Assert React component's output using the shallow renderer
  • Assert React component's output using the full renderer and JSX "expected" values (e.g. TestUtils.renderIntoDocument())
  • Assert React component's output using the test renderer (react-test-renderer (require unexpected-react/test-renderer)
  • Trigger events on components in shallow, full and test renderers
  • Locate components using JSX queries in shallow, full and test renderers
  • All assertions work identically with the shallow, full and test renderers, allowing you to mix and match in your tests, based on what you need.

Examples

  • Checking a simple render
var todoList = TestUtils.renderIntoDocument(
);

expect(
  todoList,
  'to have rendered', 
  <div className='items'>
    <TodoItem id={1}>
      <span className="label">Buy flowers</span>
    </TodoItem>
    <TodoItem id={2}>
      <span className="label">Mow the lawn</span>
    </TodoItem>
    <TodoItem id={3}>
      <span className="label">Buy groceries</span>
    </TodoItem>
  </div>
);
  • Direct rendering for shallow and deep rendering:
    expect(
    <TodoList items={todoItems} />,
    'when rendered',
    'to have rendered', 
    <div className='items'>
      <TodoItem id={1} label="Buy flowers" />
      <TodoItem id={2} label="Mow the lawn" />
      <TodoItem id={3} label="Buy groceries" />
    </div>
    );
  • Triggering an event on a button inside a subcomponent (using the eventTarget prop to identify where the event should be triggered)
expect(
  todoList,
  'with event click',
  'on', <TodoItem id={2}><span className="label" eventTarget /></TodoItem>,
  'to contain',
  <TodoItem id={2}>
    <div className='completed'>
      <span>Completed!</span>
    </div>
  </TodoItem>
);
  • Locating a component with queried for then validating the render
expect(
  todoList,
  'queried for', <TodoItem id={2} />,
  'to have rendered',
  <TodoItem id={2}>
    <div className='completed'/>
  </TodoItem>
);
  • Locating a component and then checking the state of the component with the full renderer
expect(todoList,
  'with event click',
  'on', <TodoItem id={1}><span className="label" eventTarget /></TodoItem>,
  'queried for', <TodoItem id={1} />
).then(todoItem => {
  // Here we're checking the state, but we could perform
  // any operation on the instance of the component.
  expect(todoItem.state, 'to satisfy', { completed: true });
});
  • Calling an event and validating the output using the test renderer
const unexpected = require('unexpected');
const React = require('react');
const TestRenderer = require('react-test-renderer');
const expect = unexpected.clone().use(require('unexpected-react/test-renderer'));

describe('ClickCounterButton', function () {

  it('shows the increased click count after a click event', function () {
    const renderer = TestRenderer.create(<ClickCounterButton />);
    expect(renderer, 
        'with event', 'click',
        'to have rendered',
        <button>Clicked {1} time</button>
    );
  });
});

Usage

npm install --save-dev unexpected unexpected-react

Initialising

With the shallow renderer


var unexpected = require('unexpected');
var unexpectedReact = require('unexpected-react');

var React = require('react');
var ReactTestUtils = require('react-dom/test-utils');

// Require the component we want to test
var MyComponent = require('../MyComponent');

// Declare our `expect` instance to use unexpected-react
var expect = unexpected.clone()
    .use(unexpectedReact);

describe('MyComponent', function () {
    it('renders a button', function () {
        var renderer = ReactTestUtils.createRenderer();
        renderer.render(<MyComponent />);
        expect(renderer, 'to have rendered', <button>Click me</button>);
    });
});

With the test renderer

If you want to use the react-test-renderer, then require('unexpected-react/test-renderer')


var unexpected = require('unexpected');

// Note that for the test-renderer, we need a different `require`
var unexpectedReact = require('unexpected-react/test-renderer');

var React = require('react');
var TestRenderer = require('react-test-renderer');

var MyComponent = require('../MyComponent');

// define our instance of the `expect` function to use unexpected-react
const expect = unexpected.clone()
    .use(unexpectedReact);


describe('MyComponent', function () {
    it('renders a button', function () {
        var renderer = TestRenderer.create(<MyComponent />);
        expect(renderer, 'to have rendered', <button>Click me</button>);
    });
});

With the full virtual DOM (all custom components AND the DOM elements)

If you want to assert over the whole virtual DOM, then you need to emulate the DOM (note this library is not designed for use in the browser - it may be possible, but at the very least, you'll need to disable the react-devtools)

If you don't need the virtual DOM, and you're just using the shallow renderer, then the order of the requires is not important, and you obviously don't need the emulateDom.js require.

The order of require's is important. unexpected-react must be required before react is required. That means unexpected-react must be required before any other file is required that requires React (e.g. your components!)

(You can also use the shallow renderer interchangeably with this setup)

// First require your DOM emulation file (see below)
require( '../testHelpers/emulateDom');

var unexpected = require('unexpected');

// then require unexpected-react
var unexpectedReact = require('unexpected-react');

// then react
var React = require('react');

// ...and optionally the addons
var TestUtils = require('react-dom/test-utils');

// then our component(s)
var MyComponent = require('../MyComponent);

// define our instance of the `expect` function to use unexpected-react
const expect = unexpected.clone()
    .use(unexpectedReact);

describe('MyComponent', function () {
    it('renders a button', function () {
        var component = TestUtils.renderIntoDocument(<MyComponent />);

        // All custom components and DOM elements are included in the tree,
        // so you can assert to whatever level you wish
        expect(component, 'to have rendered', 
          <MyComponent>
            <button>Click me</button>
          </MyComponent>);
    });
});

Using with Jest

unexpected-react works just the same with jest, complete with snapshot support (and you don't need your own DOM emulation, as jest has that built in). To use jest with the shallow and full renderers and include snapshot support, simply require unexpected-react/jest. Snapshotting the shallow renderer and the full DOM rendering works out of the box, no need to add any extra packages.

e.g.

const unexpectedReact = require('unexpected-react/jest');

const expect = require('unexpected')
  .clone()
  .use(unexpectedReact);

This expect will then be used instead of the default one provided by jest.

If you want to use the test renderer (the same as jest snapshots use), require unexpected-react/test-renderer-jest.

e.g.

const unexpectedReact = require('unexpected-react/test-renderer-jest');

const expect = require('unexpected')
  .clone()
  .use(unexpectedReact);

Emulating the DOM

If you're using Jest, you can skip this part, as it comes with built in jsdom support.

For React v16, we recommend using jsdom-global and the requireAnimationFrame polyfill from Erik Möller, Paul Irish and Tino Zijdel. For previous versions, you can use the boilerplate presented here.

The emulateDom file depends on whether you want to use domino, or jsdom. If you're using Jest, jsdom is built in, so you can ignore this section.

For jsdom:

// emulateDom.js - jsdom variant

if (typeof document === 'undefined') {

    const jsdom = require('jsdom').jsdom;
    global.document = jsdom('');
    global.window = global.document.defaultView;

    for (let key in global.window) {
        if (!global[key]) {
            global[key] = global.window[key];
        }
    }
}

For domino:

// emulateDom.js - domino variant

if (typeof document === 'undefined') {

    const domino = require('domino');
    global.window = domino.createWindow('');
    global.document = global.window.document;
    global.navigator = { userAgent: 'domino' };

    for (let key in global.window) {
        if (!global[key]) {
            global[key] = global.window[key];
        }
    }
}

React Compatibility

v5.x.x is compatible with React v16 and up v4.x.x is compatible with React v15.5 and up v3.x.x is compatible with React v0.14.x and v15. Warning with v15.5, but supported v2.x.x is compatible with React v0.13.x and v0.14.x

It is not planned to make further releases of the v2 and v3 branch, but if you still need 0.13 / 0.14 support, and are missing things from v4/5, please raise an issue.

Tests

For the shallow renderer, you can assert on the renderer itself (you can also write the same assertion for the result of getRenderOutput())

var renderer = TestUtils.createRenderer();

renderer.render(<MyButton />);

expect(renderer, 'to have rendered',
  <button>
      Button was clicked 1 times
  </button>
);

If this fails for some reason, you get a nicely formatted error, with the differences highlighted:

expected
<button onClick={function bound onClick() { /* native code */ }}>
  Button was clicked 0 times
</button>
to have rendered <button>Button was clicked 1 times</button>

<button onClick={function bound onClick() { /* native code */ }}>
  Button was clicked 0 times // -Button was clicked 0 times
                             // +Button was clicked 1 times
</button>

You can also use when rendered to directly render a component to a shallow renderer:


expect(<MyButton />, 
  'when rendered',
  'to have rendered',
  <button>
      Button was clicked 1 times
  </button>
);

If you've emulated the DOM, you can write a similar test, but using ReactDOM.render() (or TestUtils.renderIntoDocument())

var component = TestUtils.renderIntoDocument(<MyButton/>)
expect(component, 'to have rendered',
  <button>
      Button was clicked 1 times
  </button>
);
expected
<MyButton>
  <button onClick={function bound onClick() { /* native code */ }}>
    Button was clicked 0 times
  </button>
</MyButton>
to have rendered <button>Button was clicked 1 times</button>

<MyButton>
  <button onClick={function bound onClick() { /* native code */ }}>
    Button was clicked 0 times // -Button was clicked 0 times
                               // +Button was clicked 1 times
  </button>
</MyButton>

Note the major difference between the shallow renderer and the "normal" renderer, is that child components are also rendered. That is easier to see with these example components:


var Text = React.createClass({
   render() {
       return <span>{this.props.content}</span>;
   }
});

var App = React.createClass({
   render() {
        return (
            <div className="testing-is-fun">
              <Text content="hello" />
              <Text content="world" />
            </div>
        );
   }
});

Rendering the App component with the shallow renderer will not render the spans, only the Text component with the props. If you wanted to test for the content of the span elements, you'd need to use TestUtils.renderIntoDocument(...), or ReactDOM.render(...)

Because unexpected-react` by default ignores wrapper elements, and also "extra" children (child nodes that appear in the actual render, but are not in the expected result), it is possible to test both scenarios with the full renderer. To demonstrate, all the following tests will pass:

var component = TestUtils.renderIntoDocument(<App />);

// renders the Text components with the spans with the full renderer
expect(component, 'to have rendered', 
  <App>
    <div className="testing-is-fun">
      <Text content="hello">
        <span>hello</span>
      </Text>
      <Text content="world">
        <span>world</span>
      </Text>
    </div>
  </App>
);
// renders the Text nodes with the full renderer'

expect(component, 'to have rendered', 
  <div className="testing-is-fun">
      <Text content="hello" />
      <Text content="world" />
  </div>
);
// renders the spans with the full renderer

expect(component, 'to have rendered', 
  <div className="testing-is-fun">
      <span>hello</span>
      <span>world</span>
  </div>
);

The first test shows the full virtual DOM that gets rendered. The second test skips the <App> "wrapper" component, and leaves out the <span> children of the <Text> components. The third tests skips both the <App> wrapper component, and the <Text> wrapper component.

Stateless components

Because stateless components can't be instantiated, renderIntoDocument won't return an instance back. Using the shallow renderer works as shown in the first example. For full rendering, use the when deeply rendered to render the component

expect(<StatelessComponent name="Daniel" />, 
  'when deeply rendered', 
  'to have rendered',
  <div>Hello, Daniel!</div>);

Cleaning up

When using the normal renderer, unexpected-react makes use of react-render-hook, which utilises the code from the React devtools. As there is no way for react-render-hook to know when a test is completed, it has to keep a reference to every rendered component. Whilst this shouldn't normally be an issue, if you use a test runner that keeps the process alive (such as wallaby.js), it is a good idea to call unexpectedReact.clearAll() in a global beforeEach() or afterEach() block. This clears the cache of rendered nodes.

Roadmap / Plans

  • [DONE] There are some performance optimisations to do. The performance suffers a bit due to the possible asynchronous nature of the inline assertions. Most of the time these will be synchronous, and hence we don't need to pay the price.
  • [DONE] queried for implementation
  • [DONE] Directly calling events on both the shallow renderer, and the full virtual DOM renderer
  • [DONE] Support Snapshot testing in Jest
  • Cleanup output - where there are no differences to highlight, we could skip the branch

Contributing

We welcome pull requests, bug reports, and extra test cases. If you find something that doesn't work as you believe it should, or where the output isn't as good as it could be, raise an issue!

Thanks

Huge thanks to @Munter for unexpected-dom, and along with @dmatteo from Podio for handing over the unexpected-react name.

Unexpected is a great library to work with, and I offer my sincere thanks to @sunesimonsen and @papandreou, who have created an assertion library that makes testing JavaScript a joy.

License

MIT

changelog

Changelog

v0.2.x

Original version from podio

v0.3.0

v0.3.1

  • Fix dependencies - issue seen when using npm 2.x (thanks to @faceyspacey for reporting)

v0.3.2

  • Fix error handling when react-render-hook was not injected

v0.4.0

  • Update to v0.4.1 of unexpected-htmllike-jsx-adapter, to add support for iterators as children Thanks to @jkimbo for the failing test, and @Lugribossk for reporting

v0.4.1

  • Force dependency update to unexpected-htmllike 0.3.2 - important bugfix for 'to contain', when a child element is not identical, but actually matches with the to contain flags that are in place.

v0.5.0

  • Update to unexpected-htmllike 0.4.0, and add className diffing, using class semantics. i.e. order does not matter, extra classes are ignored by default (not with exactly style assertions).

v0.5.1

  • Patch version of unexpected-htmllike-jsx-adapter to support flattening children (only a package.json update)

v0.5.2

  • Patch version of unexpected-htmllike to ^0.5.0 to fix issue with reordered children

v1.0.0

  • Update to unexpected-htmllike v1.0.0 - much faster due to attempting everything sync first, before reverting
    to async if an async assertion is encountered. You no longer need to return the result of the expect, unless you're actually doing an asynchronous expect.it() assertion somewhere in your expected value.

v1.0.1

  • Patch version of unexpected-htmllike to ^1.1.0 (improves output for function props, and props with undefined values - #15)

v2.0.0

  • Props / Attributes are now tested with to satisfy semantics, unless exactly is specified, in which case to equal is used. This is probably what was expected, but could mean some tests that previously failed now pass, hence the major version.

  • queried for support for both shallow and deep renderers

  • with event event triggering for shallow and deep renderers

v2.0.1

  • Fix rendering numerical 0, via a fix from react-devtools

v3.0.0

  • Drop support for React 0.13.x and add support for React v15 (thanks Gustav Nikolaj #19)
  • Remove need for Symbol polyfill (fixed #18)

v3.0.1

  • Remove unexpected-documentation-site-generator from the dependencies (thanks @jkimbo #20)

v3.0.2

  • Update various dependencies, so that React 0.13 is properly no longer supported, and React v15 is properly supported, without peerDep warnings (thanks @choffmeister)

v3.1.0

  • Enable using queried for and with event as the resolution of the promise.

v3.1.1

  • Update to htmllike 2.0.2, fixes output issue

v3.1.2

  • Fix for combining queried for and with event in the deep renderer (#23 - thanks @janmeier for reporting)

v3.1.3

  • Fix for 'not to contain' and 'to contain with all children' after event

v3.2.0

  • Update docs for rendering stateless components (thanks @2color)
  • Add support for eventTarget in on clauses of with event
  • Add support for queryTarget in queried for

v3.2.1

  • Update to unexpected-htmllike 2.1.1 (improved output for text content diffs)

v3.2.2

  • Fixed issue with queried for when the resulting promise is used, not returning the correct object. Note that if the result of the query is an HTMLElement, it is now (correctly) the HTMLElement, and not an opaque object that could be used in a further to have rendered style assertion. As this should have been the case, although the tests have been changed, this is considered a patch version issue as this was the intended behaviour - the tests were sadly wrong. (#26)

  • Fixed issue that multiple events with arguments would not always use the correct object as the result of the promise. e.g.

expect(component,
    'with event click', { /* ... */ },
    'with event click', { /* ... */ })
   .then(result => {
      /* Here the result would have been undefined */
    });

v3.2.3

  • Fixed issue with default flags for on clauses in events. It now uses the same default matching flags as all other assertions. All assertions now use a single function to calculate the options, so the matching flags are always the same based on the with all children, with all wrappers, and exactly flags

v3.2.4

  • Fixed and updated the docs, and with huge thanks to @sunesimonsen all the examples in the docs are now tested

v3.3.0

  • Added support for the react-test-renderer
  • Fixed issue #28 (immutable lists mixed with other child components)

v3.3.1

  • Added missing test-renderer file for requireing

v3.3.2

  • Fix issue with asserting against objects that do not have Object as their prototype (thanks to Martin Eigenmann @eigenmannmartin for reporting and fixing!)

v3.4.0

  • Jest snapshot support ('to match snapshot' and 'to satisfy snapshot' assertions)
  • Major refactor so assertions for all renderers (shallow, full DOM, test and raw for snapshots) use the same code

v3.5.0

  • Add when rendered, when deeply rendered and to render as assertions to enable direct rendering within the assertion
  • Add helpful error messages when using assertions that require a different require to be used (e.g. using jest snapshot assertions without requiring unexpected-react/jest
  • Add helpful error message when validating against the test renderer .toJSON() method output - should be called with the renderer directly

v3.5.1

  • Bump version of js-writer to fix bug with snapshots with booleans

v3.5.2

  • Fix issue with snapshot comments (JSX representation) not always saving correctly (thanks @sunesimonsen for reporting, help tracking it down and the fix!)

v4.0.0

  • (breaking) Drop support for React 0.14 and <= 15.4. React 15.5 without any warnings

v4.0.1

  • Fix peerDependency versions for react (thanks @gustavnikolaj)

v4.0.2

  • Fix snapshot support for Jest 20

v4.1.0

  • Return the renderer output as the fulfillment value of the promise for to render as and to have rendered, allowing further assertions on the output after the main assertion (thanks @papandreou!)

v5.0.0-rc1

  • Initial support for React v16 (Fiber).

v5.0.0-rc2

  • Fix incorrect dependency for reactrendered adapter

  • Initial support for React v16 (Fiber).

v5.0.0-rc3

  • Fix peer dependencies
  • Add react-native (shallow only) support require('unexpected-react/react-native') - thanks @janmonschke!

v5.0.0-rc4

  • Bump unexpected-htmllike to 2.1.3 to fix missing assertion output on some assertions (errorMode was being overwritten)

v5.0.0

  • Make peerDeps react 16 official

v5.0.1

  • Bump js-writer to fix issue serialising ReactElements in snapshots (thanks Aravind Ravi Sulekha for reporting)

v5.0.2

  • Bump unexpected-htmllike-reactrendered-adapter for React 16.5 support

v5.0.3

  • Bump unexpected-htmllike to 2.2.0 to update support for unexpected versions

v5.0.4

  • Compatibility fixes for unexpected 11 (thanks @papandreou)

v6.0.0

  • Compatibility for React 16.9. jest snapshot tests are still failing, but this adds support for React 16.9 and up, so releasing anyway with hopefully a fix later for snapshot tests

v6.0.1

  • Bump the peer deps to include react 16.9

v6.0.2

  • Bump dependencies