department-of-veterans-affairs/vets-website

View on GitHub
src/platform/testing/unit/helpers.js

Summary

Maintainability
B
5 hrs
Test Coverage
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import PropTypes from 'prop-types';
import React from 'react';
import { createMemoryHistory } from 'history-v4';
import ReactTestUtils from 'react-dom/test-utils';
import sinon from 'sinon';

import environment from '@department-of-veterans-affairs/platform-utilities/environment';
import { $ } from '@department-of-veterans-affairs/platform-forms-system/ui';

chai.use(chaiAsPromised);

const { expect } = chai;

/**
 * Wraps the given children with a new component with context from
 * context and contextTypes.
 *
 * @param {object} context The context object for the new component
 * @param {object} contextTypes An object with a prop type description of
 * @param {React.Element} children React elements that the new component will wrap
 * @returns {React.Element} A new React element that wraps children with context
 */
function wrapWithContext(context, contextTypes, children) {
  class WrapperWithContext extends React.Component {
    getChildContext() {
      return context;
    }

    render() {
      return children;
    }
  }

  WrapperWithContext.childContextTypes = contextTypes;

  return React.createElement(WrapperWithContext);
}

/**
 * Wraps the given component with a component with an emtpy
 * router object in context.
 *
 * @param {React.Component} component The component to wrap with router context
 * @returns {React.Element} A new React element that wraps component
 */
function wrapWithRouterContext(component) {
  const context = { router: {} };
  const contextTypes = { router: PropTypes.object };
  return wrapWithContext(context, contextTypes, component);
}

/**
 * Fills a date given a partial id and a date string
 *
 * @param {object} formDom Returned from findDOMNode(form).
 *                         Used to find the elements in the form.
 * @param {string} partialID The ID of the date elements without 'Month', 'Day', or 'Year'
 *                           e.g. 'root_children_0_childDateOfBirth'
 * @param {string} dateString A string representation of the date.
 *                           e.g. '2012-1-28'
 */
function fillDate(formDOM, partialId, dateString) {
  const date = dateString.split('-');
  const inputs = Array.from(formDOM.querySelectorAll('input, select'));
  ReactTestUtils.Simulate.change(
    inputs.find(i => i.id === `${partialId}Month`),
    {
      target: {
        value: date[1],
      },
    },
  );
  ReactTestUtils.Simulate.change(inputs.find(i => i.id === `${partialId}Day`), {
    target: {
      value: date[2],
    },
  });
  ReactTestUtils.Simulate.change(
    inputs.find(i => i.id === `${partialId}Year`),
    {
      target: {
        value: date[0],
      },
    },
  );
}

/**
 * Allows the user to change a dropdown input to the value provided
 *
 * @param {object} form
 * @param {string} selector
 * @param {string} value
 */
export function changeDropdown(form, selector, value) {
  const field = form.find(selector);
  field.simulate('change', {
    target: { value },
  });
}

/**
 * A function to mock the global fetch function and return
 * the value provided in returnVal.
 *
 * @param returnVal The value to return from the fetch promise
 * @param {boolean} [shouldResolve=true] Returns a rejected promise if this is false
 */
function mockFetch(returnVal, shouldResolve = true) {
  const fetchStub = sinon.stub(global, 'fetch');
  fetchStub.callsFake(url => {
    let response = returnVal;
    if (!response) {
      response = new Response();
      response.ok = false;
      response.url = url;
      response.status = 404;
      response.statusText = 'Not Found';
    }

    return shouldResolve ? Promise.resolve(response) : Promise.reject(response);
  });
}

export function setFetchJSONResponse(stub, data = null) {
  const response = new Response();
  response.ok = true;
  response.url = environment.API_URL;
  if (data) {
    response.headers.set('Content-Type', 'application/json');
    response.json = () => Promise.resolve(data);
  }
  stub.resolves(response);
}

export function setFetchJSONFailure(stub, data) {
  const response = new Response(null, {
    headers: { 'content-type': ['application/json'] },
  });
  response.ok = false;
  response.url = environment.API_URL;
  response.json = () => Promise.resolve(data);
  stub.resolves(response);
}

export function setFetchBlobResponse(stub, data) {
  const response = new Response();
  response.ok = true;
  response.url = environment.API_URL;
  response.blob = () => Promise.resolve(data);
  stub.resolves(response);
}

export function setFetchBlobFailure(stub, error) {
  const response = new Response();
  response.ok = false;
  response.url = environment.API_URL;
  response.blob = () => Promise.reject(new Error(error));
  stub.resolves(response);
}

/**
 * Resets the fetch mock set with mockFetch
 */
function resetFetch() {
  if (global.fetch.isSinonProxy) {
    global.fetch.restore();
  }
}

const getApiRequestObject = returnVal => ({
  headers: {
    get: () => 'application/json',
  },
  ok: true,
  json: () => Promise.resolve(returnVal),
  url: environment.API_URL,
});

/**
 * This doesn't so much _mock_ the function as it does set up the fetch to return what we
 * need it to from apiRequest(). Feel free to rename this to something more appropriate.
 *
 * @param {} returnVal The value to return from the json promise
 * @param {boolean} [shouldResolve=true] Returns a rejected promise if this is false
 */
function mockApiRequest(returnVal, shouldResolve = true) {
  const returnObj = getApiRequestObject(returnVal);
  mockFetch(returnObj, shouldResolve);
}

/**
 * @typedef {Object} Response
 * @property {} response - The value the fake fetch should return
 * @property {boolean} shouldResolve - Whether the fetch promise should resolve or not
 * ---
 * @param {Response[]} responses - An array of responses which subsequent fetch calls should return
 */
function mockMultipleApiRequests(responses) {
  mockFetch();
  responses.forEach((res, index) => {
    const { response, shouldResolve } = res;
    const formattedResponse = getApiRequestObject(response);
    global.fetch
      .onCall(index)
      .returns(
        shouldResolve
          ? Promise.resolve(formattedResponse)
          : Promise.reject(formattedResponse),
      );
  });
}

/**
 * Mocks event listeners for the target being passed (e.g., a mock window).
 *
 * @param {object} target - The object to supplement with event listeners
 * @returns {object} The target with a mock event listener
 */
const mockEventListeners = (target = {}) => {
  const eventListeners = {};
  return {
    ...target,
    eventListeners,
    addEventListener: (eventType, callback) => {
      if (eventListeners[eventType]) {
        eventListeners[eventType].push(callback);
      } else {
        eventListeners[eventType] = [callback];
      }
    },
    simulate: (eventType, eventObject) => {
      if (eventListeners[eventType]) {
        eventListeners[eventType].forEach(callback => callback(eventObject));
      }
    },
  };
};

/**
 * Creates a history object and attaches a spy to replace and push.
 * The history object is fully functional, not stubbed.
 *
 * @export
 * @param {string} [path='/'] - The initial url to use for the history
 * @returns {History} A History object
 */
const createTestHistory = (path = '/') => {
  const history = createMemoryHistory({ initialEntries: [path] });
  sinon.spy(history, 'replace');
  sinon.spy(history, 'push');

  return history;
};

/**
 * Input a string value into a va-text-input component.
 * @param {any} container - React Testing Library container
 * @param {string} value - string value to enter in the input field
 * @param {string} selector - string containing selector to match
 */
const inputVaTextInput = (container, value, selector = 'va-text-input') => {
  const vaTextInput = $(selector, container);
  vaTextInput.value = value;

  const event = new CustomEvent('input', {
    bubbles: true,
    detail: { value },
  });
  vaTextInput.dispatchEvent(event);
};

/**
 * Select a checkbox within a given group
 * @param {object} checkboxGroup - element containing the group
 * @param {string} keyName - unique key
 */
const checkVaCheckbox = (checkboxGroup, keyName) => {
  checkboxGroup.__events.vaChange({
    target: {
      checked: true,
      dataset: { key: keyName },
    },
    detail: { checked: true },
  });
};

export {
  chai,
  checkVaCheckbox,
  createTestHistory,
  expect,
  fillDate,
  inputVaTextInput,
  mockFetch,
  mockApiRequest,
  mockMultipleApiRequests,
  mockEventListeners,
  resetFetch,
  wrapWithContext,
  wrapWithRouterContext,
};