Skip to content

Instantly share code, notes, and snippets.

@MatthewCallis
Last active August 16, 2023 17:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MatthewCallis/47e9b54a73b1c6516ad3a210c019f4b3 to your computer and use it in GitHub Desktop.
Save MatthewCallis/47e9b54a73b1c6516ad3a210c019f4b3 to your computer and use it in GitHub Desktop.
Jest Testing Helpers for React Components with Promises
import fetch from 'isomorphic-fetch';
expost const get = async (url, headers = {}) => fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
...headers,
},
});
module.exports = {
automock: false,
bail: true,
preset: 'ts-jest',
testMatch: [
'<rootDir>/test/**/*.(spec|test).(j|t)s?(x)',
'<rootDir>/src/**/*.(spec|test).(j|t)s?(x)',
],
setupFiles: [
'<rootDir>/testSetup.js',
],
transformIgnorePatterns: [
'/node_modules/',
],
moduleDirectories: [
'node_modules',
'src',
],
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: [
'/node_modules/',
'/test/',
],
coverageThreshold: {
global: {
statements: 90,
branches: 90,
functions: 90,
lines: 90
}
},
testURL: 'http://my.domain.tld',
globals: {
'ts-jest': {
diagnostics: {
// Ignore the "Object is possibly 'undefined'" typescript warning in tests
// https://huafu.github.io/ts-jest/user/config/diagnostics
ignoreCodes: [2532],
},
},
},
};
// Enzyme does not always facilitate component updates based on state changes resulting from Promises,
// this forces the re-render with the new data.
// From: https://stackoverflow.com/a/51045733/198130
// Need to look for setImmediate in all possible locations. https://github.com/browserify/timers-browserify/pull/28
// OLD: const setImmediate = self && self.setImmediate || global && global.setImmediate || window && window.setImmediate || setInterval;
// OLD: export const flushPromises = () => new Promise(resolve => setImmediate(resolve));
// Equivalent to:
// export const flushPromises = () => new Promise(resolve => { setTimeout(resolve, 0); });
export const flushPromises = () => new Promise(process.nextTick);
import React from 'react';
import { mount } from 'enzyme';
import * as api from '../src/api.js';
import Component from '../src/component.jsx';
import { flushPromises } from 'test-helpers.js';
jest.mock('../src/api');
// For mocking new Date()
const mockDate = (isoDate) => {
global.Date = class extends RealDate {
constructor () {
return new RealDate(isoDate);
}
}
};
// API Domain
const url = 'https://api.domain.tld';
describe('Component', () => {
beforeAll(() => {
// Stub Timers, used with: jest.runAllTimers(); / jest.runOnlyPendingTimers();
jest.useFakeTimers();
// Do not reach out to the Internet at all.
nock.disableNetConnect();
nock.cleanAll();
const request = nock(`${url}`)
.persist()
.filteringRequestBody(() => '*')
.defaultReplyHeaders({
'Content-Type': 'application/json',
})
.get('/')
.reply(200);
request.on('error', (err) => {
console.log('NOCK ROOT REQUEST ERROR:', err); // eslint-disable-line no-console
});
});
beforeEach(() => {
// Testing against Date.now() by setting a known value
jest.spyOn(Date, 'now').mockReturnValue(new Date('2021-04-20').getTime());
// Testing window Modal methods
jest.spyOn(window, 'confirm').mockImplementation(() => true);
jest.spyOn(window, 'prompt').mockImplementation(() => '🦧');
jest.spyOn(window, 'alert').mockImplementation(() => true);
// Testing Redirects with window.location.assign('URL'), preferred to setting window.location.href directly
delete global.window.location;
window.location = { assign: jest.fn() };
jest.spyOn(window.location, 'assign').mockImplementation(() => true);
// Crypto should be random, but that is difficult to test, customize to fit needs.
jest.spyOn(window.crypto, 'getRandomValues').mockImplementation((array) => array.set([0,1,2,3,4,5,6,7,8,9].slice(0, array.length), 0));
// Math.random() can be tricky to test against as well, you will want to pick the value in your tests directly.
jest.spyOn(Math, 'random').mockImplementation(() => 0.5);
});
afterEach(() => {
// Reset Timers
jest.clearAllTimers();
// Reset back, see: https://jestjs.io/docs/mock-function-api#mockfnmockrestore
Date.now.mockRestore();
window.alert.mockRestore();
window.prompt.mockRestore();
window.confirm.mockRestore();
window.location.assign.mockRestore();
window.crypto.getRandomValues.mockRestore();
Math.random.mockRestore();
// Reset the date back to the real date.
global.Date = RealDate;
});
it('renders', async () => {
// Spoof new Date() values
const created_at = new Date('2021-08-03T00:59:52.232Z');
mockDate(created_at);
// Mock the API data with custom data.
const data = [{ id: '123' }]
jest.spyOn(api, 'get').mockImplementation(() => Promise.resolve({
json: () => Promise.resolve(data)
}));
const wrapper = mount(<Component />);
// Give Enzyme time to run all the promises.
await flushPromises();
wrapper.update();
expect(wrapper.children()).toHaveLength(1);
// Clear our mock
api.get.mockClear();
});
});
// Handle Fetches
import 'isomorphic-fetch';
// Stub Local Storage
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
removeItem(key) {
delete this.store[key];
}
setItem(key, value) {
this.store[key] = value.toString();
}
key(index) {
return Object.keys(this.store)[index];
}
get length() {
return Object.keys(this.store).length;
}
}
global.localStorage = new LocalStorageMock();
// Catch any browser pushState use.
window.history.pushState = () => null;
// Setup a default referrer
global.document = {
referrer: 'some.external.tld',
};
global.location = {
origin: 'my.domain.tld',
};
// Defauly Crypto Values
global.crypto = {
getRandomValues() {
return ['1234'];
},
};
// Stub Analytics
global.analytics = {
invoke: jest.fn(),
track: jest.fn(),
};
// Catch any undefined window.requestAnimationFrame use.
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
// MIT license
let lastTime = 0;
const vendors = ['ms', 'moz', 'webkit', 'o'];
for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`];
window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`]
|| window[`${vendors[x]}CancelRequestAnimationFrame`];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (callback) => {
const currTime = new Date().getTime();
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
const id = window.setTimeout(() => {
callback(currTime + timeToCall);
},
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = (id) => {
clearTimeout(id);
};
}
// Logging for Unhandled Promise Rejections that otherwise get logged as warnings with no trace.
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled Rejection at:', p, 'reason:', reason);
});
// When --runInBand is used we can detect unhandled Promise failures.
if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
process.on('unhandledRejection', (reason) => {
throw reason;
});
// Avoid memory leak by adding too many listeners.
process.env.LISTENING_TO_UNHANDLED_REJECTION = true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment