Home Reference Source

src/unit-test-helpers.js

import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';

/**
 * Fixtures for a component
 * where the key is the description
 * and the value is the component props.
 *
 * @typedef  {Object} ComponentFixtures
 * @property {Object} * props object.
 */

/**
 * Fixtures for a reducer
 * where the key is the description
 * and the value is a ReducerFixture
 *
 * @typedef  {Object} ReducerFixtures
 * @property {ReducerFixture} * props object.
 */

/**
 * Fixture for a reducer.
 *
 * @typedef  {Object} ReducerFixture
 * @property {Object} state  Current state.
 * @property {Object} action Action object with type and payload.
 */

/**
 * Fixtures for a component
 * where the key is the description
 * and the value is the component props.
 *
 * @typedef  {Object} ComponentFixturesResults
 * @property {string} description Fixture description.
 * @property {EnzymeComponent} component enzyme shallow rendered component.
 */

/**
 * Fixtures for actions
 * where the key is the description
 * and the value is the action.
 *
 * @typedef  {Object} ActionsFixtures
 * @property {Object} * Action to run.
 */

/**
 * Shallow render a component multipile times with fixtures
 * @since 0.1.0
 * @param  {ReactComponent}    Component Component to shallow-render
 * @param  {ComponentFixtures} fixtures  Fixtures to render
 * @return {ComponentFixturesResults[]}  Shallow rendering results.
 *
 * @example
 * const components = shallowRenderComponentWithFixtures(Button, {
 *   'should render a button': { href: 'https://google.com' },
 *   'should render a button with icon': { href: 'https://github.com', icon: 'github' },
 * });
 *
 * // results
 * [{
 *   'should render a button': ShllowRendered(Button),
 *   'should render a button': ShllowRendered(ButtonWithIcon),
 * }]
 */
export const shallowRenderComponentWithFixtures = (Component, fixtures) =>
  Object.entries(fixtures).map(([description, props]) => ({
    description,
    component: shallow(<Component {...props} />),
  }));

/**
 * Test a component with fixtures and snapshots
 * @since 0.1.0
 * @param  {ReactComponent}    Component Component to test
 * @param  {ComponentFixtures} fixtures  Fixtures to test
 *
 * @example
 * describe('Button', () => testComponentSnapshotsWithFixtures(Button, {
 *   'should render a button': { href: 'https://google.com' },
 *   'should render a button with icon': { href: 'https://github.com', icon: 'github' },
 * });
 *
 * // results
 * describe('Button', () => {
 *   test('should render a button', () => {
 *     // renders button and test against snapshot
 *   });
 *   test('should render a button with icon', () => {
 *     // renders button with icon and test against snapshot
 *   });
 * });
 */
export const testComponentSnapshotsWithFixtures = (Component, fixtures) =>
  shallowRenderComponentWithFixtures(Component, fixtures).forEach(
    ({ description, component }) =>
      it(description, () => expect(toJson(component)).toMatchSnapshot())
  );

/**
 * Run an action (sync or async) and except the results to much snapshot
 * @since 0.1.0
 * @param  {Function} action Action to run
 * @return {Promise}
 * @example
 * test('should show modal', () => testActionSnapshot(showModal));
 * @example
 * test('load user', () => testActionSnapshot(loadUser));
 */
export const testActionSnapshot = async action => {
  const actionResults = action();

  // if it's an async action
  if (typeof actionResults === 'function') {
    const dispatch = jest.fn();
    await actionResults(dispatch);

    expect(dispatch.mock.calls).toMatchSnapshot();
  } else {
    expect(actionResults).toMatchSnapshot();
  }
};

/**
 * Test actions with fixtures and snapshots
 * @since 0.1.0
 * @param {ActionsFixtures} fixtures Fixtures to test
 *
 * @example
 * describe('PageActions', () => testActionSnapshotWithFixtures({
 *   'should show notifications box': () = showNotificationsBox(),
 * });
 *
 * // results
 * describe('PageActions', () => {
 *   test('should show notifications box', () => {
 *     // run showNotificationsBox() and test results against snapshot
 *   });
 * });
 *
 * @example
 * describe('UserActions', () => testActionSnapshotWithFixtures({
 *   'should login user': () = login({
 *     username: 'some-username',
 *     password: 'some-password',
 *   }),
 *   'should load update user email': () = updateEmail({
 *     username: 'some-username',
 *     oldEmail: 'some@email.com',
 *     newEmail: 'some-other@email.com',
 *   }),
 * });
 *
 * // results
 * describe('UserActions', () => {
 *   test('should login user', () => {
 *     // run login() and test results against snapshot
 *   });
 *   test('should load update user email', () => {
 *     // run updateEmail() and test results against snapshot
 *   });
 * });
 */
export const testActionSnapshotWithFixtures = fixtures =>
  Object.entries(fixtures).forEach(([description, runAction]) =>
    it(description, () => testActionSnapshot(runAction))
  );

/**
 * Test a reducer with fixtures and snapshots
 * @since 0.1.0
 * @param  {Function} reducer Reducer to test
 * @param  {ReducerFixtures} fixtures Reducer fixtures
 *
 * @example
 * describe('LoginForm reducer', () => testReducerSnapshotWithFixtures(loginReducer, {
 *   'it should update username': {
 *     action: {
 *       type: LOGIN_FORM_UPDATE_USERNAME,
 *       payload: { username: 'some-username' }
 *     }
 *   },
 *   'it should update password': {
 *     action: {
 *       type: LOGIN_FORM_UPDATE_PASSWORD,
 *       payload: { password: 'some-password' }
 *     }
 *   },
 *   'it should toggle remember-me': {
 *     state: { rememberMe: false },
 *     action: {
 *       type: LOGIN_FORM_TOGGLE_REMEMBER_ME,
 *     }
 *   },
 * }));
 *
 */
export const testReducerSnapshotWithFixtures = (reducer, fixtures) => {
  const reduce = ({ state, action = {} } = {}) => reducer(state, action);
  Object.entries(fixtures).forEach(([description, action]) =>
    it(description, () => expect(reduce(action)).toMatchSnapshot())
  );
};

/**
 * Test selectors with fixtures and snapshots
 * @since 0.1.0
 * @param  {Object} fixtures  key=fixture description value=selector runner function
 *
 * @example
 * cost state = { user: { loggedIn: true, firstName: 'Eli', lastName: 'Ohana' } };
 *
 * describe('UserSelectors', () => testSelectorsSnapshotWithFixtures({
 *   'should select is-user-logged-in': selectIsUserLoggedIn(state),
 *   'should select user-full-name': selectUserFullName(state),
 * }));
 */
export const testSelectorsSnapshotWithFixtures = fixtures =>
  Object.entries(fixtures).forEach(([description, selectorRunner]) =>
    it(description, () => expect(selectorRunner()).toMatchSnapshot())
  );