Last active
August 16, 2023 17:03
-
-
Save MatthewCallis/47e9b54a73b1c6516ad3a210c019f4b3 to your computer and use it in GitHub Desktop.
Jest Testing Helpers for React Components with Promises
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fetch from 'isomorphic-fetch'; | |
expost const get = async (url, headers = {}) => fetch(url, { | |
method: 'GET', | |
headers: { | |
Accept: 'application/json', | |
...headers, | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | |
}, | |
}, | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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