How do I mock the IntersectionObserver API in Jest? - javascript

I've read all of the relevant questions on this topic, and I realize this will probably be marked as a duplicate, but I simply cannot for the life of me figure out how to get this working.
I have this simple function that lazily loads elements:
export default function lazyLoad(targets, onIntersection) {
const observer = new IntersectionObserver((entries, self) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
onIntersection(entry.target);
self.unobserve(entry.target);
}
});
});
document.querySelectorAll(targets).forEach((target) => observer.observe(target));
return observer;
}
Example usage:
lazyLoad('.lazy-img', (img) => {
const pictureElement = img.parentElement;
const source = pictureElement.querySelector('.lazy-source');
source.srcset = source.getAttribute('data-srcset');
img.src = img.getAttribute('data-src');
});
Now, I'm trying to test the lazyLoad function using jest, but I obviously need to mock IntersectionObserver since it's a browser API, not a native JavaScript one.
The following works for testing the observe method:
let observe;
let unobserve;
beforeEach(() => {
observe = jest.fn();
unobserve = jest.fn();
window.IntersectionObserver = jest.fn(() => ({
observe,
unobserve,
}));
});
describe('lazyLoad utility', () => {
it('calls observe on each target', () => {
for (let i = 0; i < 3; i++) {
const target = document.createElement('img');
target.className = 'lazy-img';
document.body.appendChild(target);
}
lazyLoad(
'.lazy-img',
jest.fn(() => {})
);
expect(observe).toHaveBeenCalledTimes(3);
});
});
But I also want to test the .isIntersecting logic, where the callback fires... Except I don't know how to do that. How can I test intersections with jest?

Mocking stuff is so easy when you pass it as an argument:
export default function lazyLoad(targets, onIntersection, observerClass = IntersectionObserver) {
const observer = new observerClass(...)
...
}
// test file
let entries = [];
const observeFn = jest.fn();
const unobserveFn = jest.fn()
class MockObserver {
constructor(fn) {
fn(entries,this);
}
observe() { observeFn() }
unobserve() { unobserveFn() }
}
test('...',() => {
// set `entries` to be something so you can mock it
entries = ...something
lazyLoad('something',jest.fn(),MockObserver);
});

Another option is to use mockObserver mentioned above and mock in window.
window.IntersetionObserver = mockObserver
And you don't necessary need to pass observer in component props.
The important point to test if entries isIntesecting is to mock IntersectionObserver as class like mentioned above.

Related

Jest Vue Test - Can't test window.scrollTo fn()

Have a simple scroll to Element function utilizing getBoundingClientRect & window.scrollTo, but can't get any iteration of the Jest test to get any coverage beyond branch: 100. All other test coverage is at 0.
Function to be tested:
export default function scrollToEl(el) {
let elRect = el.getBoundingClientRect();
return window.scrollTo(
elRect.left + document.documentElement.scrollLeft,
elRect.top + document.documentElement.scrollTop
);
}
Jest test that doesn't provide 100% coverage in all categories:
import * as scroll from "../scrollToEl";
describe("scrollToEl test", () => {
let element;
let ev = jest.fn();
scroll.scrollToEl = jest.fn(() => {
ev;
});
beforeEach(() => {
element = document.createElement("div");
});
it("should be called", () => {
ev(element)
expect(ev).toHaveBeenCalled();
});
});
How do I get full 100% coverage across the board?
Help please.
EDIT - now with 100% coverage across the board:
JS function:
I had to alter the export default function to export const scrollToEl = function (el)
export const scrollToEl = function (el) {
// ...
};
Test:
I had to simplify the test to get 100% coverage across the board. I added and checked for window.scrollTo along with calling the actual function on newly created element (el).
import * as scroll from "../scrollToEl";
describe("scrollToEl test", () => {
beforeEach(() => {
window.scrollTo = jest.fn();
});
it("`window.scrollTo` should be called", () => {
const el = document.createElement("span");
scroll.scrollToEl(el);
expect(window.scrollTo).toHaveBeenCalled();
});
});
So, first I have an issue with your goal of wanting 100% code coverage. But this isn’t a question about that, so we’ll skip over that for now.
To get Jest to not complain about scrollTo, you have to mock it.
window.scrollTo = jest.fn();

How to check if HTMLElement has already been generated?

I have a component from 3rd party which emits "onCellEdit" event and passes a cell element as parameter.
In my event handler I want to automatically select the whole text in the input element that is generated inside of this cell.
The problem I'm having is that when my handler is triggered the input element is not yet loaded.
(cellElement as HTMLTableCellElement).querySelector('input') returns nothing since the 3rd party component needs some time I guess.
My solution now looks like this:
selectTextOnEdit(cell: HTMLTableCellElement) {
const repeater = (element: HTMLTableCellElement) => {
const inputElement = element.querySelector('input');
if (inputElement) {
inputElement.select();
} else {
setTimeout(() => { repeater(element); }, 50);
}
};
repeater(cell);
}
this function then triggers the repeater function which goes around until the input element is found. I know I'm missing some kind of a check in case the input element is never generated.. but it's not important for this question.
I highly dislike this solution and I'm sure there are better ones.
Update:
After some research I found out about "MutationObserver".
Here is my new solution:
selectTextOnEdit(cell: HTMLTableCellElement) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
const inputElement = cell.querySelector('input');
if (inputElement) {
inputElement.select();
observer.disconnect();
}
}
});
});
observer.observe(cell, {childList: true});
}
For these kind of scenarios, I like to use a utility function of waitUntil.
import { interval } from 'rxjs';
import { take } from 'rxjs/operators';
...
export const waitUntil = async (untilTruthy: Function): Promise<boolean> => {
while (!untilTruthy()) {
await interval(25).pipe(
take(1),
).toPromise();
}
return Promise.resolve(true);
}
Then in your function, it would be:
async selectTextOnEdit(cell: HTMLTableCellElement) {
await waitUntil(() => !!cell.querySelector('input'));
const inputElement = element.querySelector('input');
inputElement.select();
}
This is the same thing but slightly cleaner in my opinion. Why is it an issue that the input was never created, shouldn't always be created if the callback of selectTextOnEdit is called?
After some research I found out about "MutationObserver".
Here is my new solution:
selectTextOnEdit(cell: HTMLTableCellElement) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
const inputElement = cell.querySelector('input');
if (inputElement) {
inputElement.select();
observer.disconnect();
}
}
});
});
observer.observe(cell, {childList: true});
}

Jest mock class method on a test-by-test basis

I'm trying to mock out a utility library class with a method that returns a JSON.
Actual library structure
module.exports = class Common() {
getConfig() {
return {
real: 'data'
}
}
The file under test looks like:
const Common = require('./common');
const common = new Common();
const config = common.getConfig();
...
const someFunction = function() {
// config.real is used inside this function
}
I'm trying to mock out the Common class and return a different config JSON for each Jest test.
const fileUnderTest = require('./../fileUnderTest.js');
const Common = require('./../common.js');
jest.mock('./../common.js');
describe('something', () => {
it('test one', () => {
Common.getConfig = jest.fn().mockImplementation(() => {
return {
real : 'fake' // This should be returned for test one
};
});
fileUnderTest.someFunction(); //config.real is undefined at this point
});
it('test two', () => {
Common.getConfig = jest.fn().mockImplementation(() => {
return {
real : 'fake2' // This should be returned for test two
};
});
})
})
Is it possible to set the return value from the mock class method created by the automock of common.js at the top of the test file?
I've tried to use mockReturnValueOnce() etc.
jest.mock
In this case you don't really need to auto-mock the entire common module since you are just replacing the implementation of one method so jest.mock('./../common'); isn't necessary.
Common.getConfig
getConfig is a prototype method so getConfig exists on the prototype of Common. To mock it use Common.prototype.getConfig instead of Common.getConfig.
config in fileUnderTest.js
An instance of Common gets created and config gets set to the result of calling common.getConfig() as soon as fileUnderTest runs, which happens as soon as it gets required so the mock for Common.prototype.getConfig has to be in place before you call require('./../fileUnderTest').
const Common = require('./../common');
Common.prototype.getConfig = jest.fn().mockImplementation(() => ({ real: 'fake' }));
const fileUnderTest = require('./../fileUnderTest');
describe('something', () => {
it('should test something', () => {
fileUnderTest.someFunction(); // config.real is 'fake' at this point
});
});
Update
To mock config.real differently for each test for code like this requires that the modules be reset between tests:
describe('something', () => {
afterEach(() => {
jest.resetModules(); // reset modules after each test
})
it('test one', () => {
const Common = require('./../common');
Common.prototype.getConfig = jest.fn().mockImplementation(() => ({ real: 'fake' }));
const fileUnderTest = require('./../fileUnderTest');
fileUnderTest.someFunction(); // config.real is 'fake'
});
it('test two', () => {
const Common = require('./../common');
Common.prototype.getConfig = jest.fn().mockImplementation(() => ({ real: 'fake2' }));
const fileUnderTest = require('./../fileUnderTest');
fileUnderTest.someFunction(); // config.real is 'fake2'
})
})
Resetting the modules is necessary because once a module is required it is added to the module cache and that same module gets returned each time it is required unless the modules are reset.

Jest: restore original module implementation on a manual mock

I have a pretty common testing use case and I am not sure what's the best approach there.
Context
I would like to test a module that depends on a userland dependency. The userland dependency (neat-csv) exports a single function that returns a Promise.
Goal
I want to mock neat-csv's behavior so that it rejects with an error for one single test. Then I want to restore the original module implementation.
AFAIK, I can't use jest.spyOn here as the module exports a single function.
So I thought using manual mocks was appropriated and it works. However I can't figure it out how to restore the original implementation over a manual mock.
Simplified example
For simplicity here's a stripped down version of the module I am trying to test:
'use strict';
const neatCsv = require('neat-csv');
async function convertCsvToJson(apiResponse) {
try {
const result = await neatCsv(apiResponse.body, {
separator: ';'
});
return result;
} catch (parseError) {
throw parseError;
}
}
module.exports = {
convertCsvToJson
};
And here's an attempt of testing that fails on the second test (non mocked version):
'use strict';
let neatCsv = require('neat-csv');
let { convertCsvToJson } = require('./module-under-test.js');
jest.mock('neat-csv', () =>
jest.fn().mockRejectedValueOnce(new Error('Error while parsing'))
);
const csv = 'type;part\nunicorn;horn\nrainbow;pink';
const apiResponse = {
body: csv
};
const rejectionOf = (promise) =>
promise.then(
(value) => {
throw value;
},
(reason) => reason
);
test('mocked version', async () => {
const e = await rejectionOf(convertCsvToJson(apiResponse));
expect(neatCsv).toHaveBeenCalledTimes(1);
expect(e.message).toEqual('Error while parsing');
});
test('non mocked version', async () => {
jest.resetModules();
neatCsv = require('neat-csv');
({ convertCsvToJson } = require('./module-under-test.js'));
const result = await convertCsvToJson(apiResponse);
expect(JSON.stringify(result)).toEqual(
'[{"type":"unicorn","part":"horn"},{"type":"rainbow","part":"pink"}]'
);
});
I am wondering if jest is designed to do such things or if I am going the wrong way and should inject neat-csv instead ?
What would be the idiomatic way of handling this ?
Yes, Jest is designed to do such things.
The API method you are looking for is jest.doMock. It provides a way of mocking modules without the implicit hoisting that happens with jest.mock, allowing you to mock in the scope of tests.
Here is a working example of your test code that shows this:
const csv = 'type;part\nunicorn;horn\nrainbow;pink';
const apiResponse = {
body: csv
};
const rejectionOf = promise =>
promise.then(value => {
throw value;
}, reason => reason);
test('mocked version', async () => {
jest.doMock('neat-csv', () => jest.fn().mockRejectedValueOnce(new Error('Error while parsing')));
const neatCsv = require('neat-csv');
const { convertCsvToJson } = require('./module-under-test.js');
const e = await rejectionOf(convertCsvToJson(apiResponse));
expect(neatCsv).toHaveBeenCalledTimes(1);
expect(e.message).toEqual('Error while parsing');
jest.restoreAllMocks();
});
test('non mocked version', async () => {
const { convertCsvToJson } = require('./module-under-test.js');
const result = await convertCsvToJson(apiResponse);
expect(JSON.stringify(result)).toEqual('[{"type":"unicorn","part":"horn"},{"type":"rainbow","part":"pink"}]');
});

How to change mock implementation on a per single test basis?

I'd like to change the implementation of a mocked dependency on a per single test basis by extending the default mock's behaviour and reverting it back to the original implementation when the next test executes.
More briefly, this is what I'm trying to achieve:
Mock dependency
Change/extend mock implementation in a single test
Revert back to original mock when next test executes
I'm currently using Jest v21. Here is what a typical test would look like:
// __mocks__/myModule.js
const myMockedModule = jest.genMockFromModule('../myModule');
myMockedModule.a = jest.fn(() => true);
myMockedModule.b = jest.fn(() => true);
export default myMockedModule;
// __tests__/myTest.js
import myMockedModule from '../myModule';
// Mock myModule
jest.mock('../myModule');
beforeEach(() => {
jest.clearAllMocks();
});
describe('MyTest', () => {
it('should test with default mock', () => {
myMockedModule.a(); // === true
myMockedModule.b(); // === true
});
it('should override myMockedModule.b mock result (and leave the other methods untouched)', () => {
// Extend change mock
myMockedModule.a(); // === true
myMockedModule.b(); // === 'overridden'
// Restore mock to original implementation with no side effects
});
it('should revert back to default myMockedModule mock', () => {
myMockedModule.a(); // === true
myMockedModule.b(); // === true
});
});
Here is what I've tried so far:
mockFn.mockImplementationOnce(fn)
it('should override myModule.b mock result (and leave the other methods untouched)', () => {
myMockedModule.b.mockImplementationOnce(() => 'overridden');
myModule.a(); // === true
myModule.b(); // === 'overridden'
});
Pros
Reverts back to original implementation after first call
Cons
It breaks if the test calls b multiple times
It doesn't revert to original implementation until b is not called (leaking out in the next test)
jest.doMock(moduleName, factory, options)
it('should override myModule.b mock result (and leave the other methods untouched)', () => {
jest.doMock('../myModule', () => {
return {
a: jest.fn(() => true,
b: jest.fn(() => 'overridden',
}
});
myModule.a(); // === true
myModule.b(); // === 'overridden'
});
Pros
Explicitly re-mocks on every test
Cons
Cannot define default mock implementation for all tests
Cannot extend default implementation forcing to re-declare each mocked method
Manual mocking with setter methods (as explained here)
// __mocks__/myModule.js
const myMockedModule = jest.genMockFromModule('../myModule');
let a = true;
let b = true;
myMockedModule.a = jest.fn(() => a);
myMockedModule.b = jest.fn(() => b);
myMockedModule.__setA = (value) => { a = value };
myMockedModule.__setB = (value) => { b = value };
myMockedModule.__reset = () => {
a = true;
b = true;
};
export default myMockedModule;
// __tests__/myTest.js
it('should override myModule.b mock result (and leave the other methods untouched)', () => {
myModule.__setB('overridden');
myModule.a(); // === true
myModule.b(); // === 'overridden'
myModule.__reset();
});
Pros
Full control over mocked results
Cons
Lot of boilerplate code
Hard to maintain on long term
jest.spyOn(object, methodName)
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
// Mock myModule
jest.mock('../myModule');
it('should override myModule.b mock result (and leave the other methods untouched)', () => {
const spy = jest.spyOn(myMockedModule, 'b').mockImplementation(() => 'overridden');
myMockedModule.a(); // === true
myMockedModule.b(); // === 'overridden'
// How to get back to original mocked value?
});
Cons
I can't revert mockImplementation back to the original mocked return value, therefore affecting the next tests
Use mockFn.mockImplementation(fn).
import { funcToMock } from './somewhere';
jest.mock('./somewhere');
beforeEach(() => {
funcToMock.mockImplementation(() => { /* default implementation */ });
// (funcToMock as jest.Mock)... in TS
});
test('case that needs a different implementation of funcToMock', () => {
funcToMock.mockImplementation(() => { /* implementation specific to this test */ });
// (funcToMock as jest.Mock)... in TS
// ...
});
A nice pattern for writing tests is to create a setup factory function that returns the data you need for testing the current module.
Below is some sample code following your second example although allows the provision of default and override values in a reusable way.
const spyReturns = returnValue => jest.fn(() => returnValue);
describe("scenario", () => {
beforeEach(() => {
jest.resetModules();
});
const setup = (mockOverrides) => {
const mockedFunctions = {
a: spyReturns(true),
b: spyReturns(true),
...mockOverrides
}
jest.doMock('../myModule', () => mockedFunctions)
return {
mockedModule: require('../myModule')
}
}
it("should return true for module a", () => {
const { mockedModule } = setup();
expect(mockedModule.a()).toEqual(true)
});
it("should return override for module a", () => {
const EXPECTED_VALUE = "override"
const { mockedModule } = setup({ a: spyReturns(EXPECTED_VALUE)});
expect(mockedModule.a()).toEqual(EXPECTED_VALUE)
});
});
It's important to say that you must reset modules that have been cached using jest.resetModules(). This can be done in beforeEach or a similar teardown function.
See jest object documentation for more info: https://jestjs.io/docs/jest-object.
Little late to the party, but if someone else is having issues with this.
We use TypeScript, ES6 and babel for react-native development.
We usually mock external NPM modules in the root __mocks__ directory.
I wanted to override a specific function of a module in the Auth class of aws-amplify for a specific test.
import { Auth } from 'aws-amplify';
import GetJwtToken from './GetJwtToken';
...
it('When idToken should return "123"', async () => {
const spy = jest.spyOn(Auth, 'currentSession').mockImplementation(() => ({
getIdToken: () => ({
getJwtToken: () => '123',
}),
}));
const result = await GetJwtToken();
expect(result).toBe('123');
spy.mockRestore();
});
Gist:
https://gist.github.com/thomashagstrom/e5bffe6c3e3acec592201b6892226af2
Tutorial:
https://medium.com/p/b4ac52a005d#19c5
When mocking a single method (when it's required to leave the rest of a class/module implementation intact) I discovered the following approach to be helpful to reset any implementation tweaks from individual tests.
I found this approach to be the concisest one, with no need to jest.mock something at the beginning of the file etc. You need just the code you see below to mock MyClass.methodName. Another advantage is that by default spyOn keeps the original method implementation but also saves all the stats (# of calls, arguments, results etc.) to test against, and keeping the default implementation is a must in some cases. So you have the flexibility to keep the default implementation or to change it with a simple addition of .mockImplementation as mentioned in the code below.
The code is in Typescript with comments highlighting the difference for JS (the difference is in one line, to be precise). Tested with Jest 26.6.
describe('test set', () => {
let mockedFn: jest.SpyInstance<void>; // void is the return value of the mocked function, change as necessary
// For plain JS use just: let mockedFn;
beforeEach(() => {
mockedFn = jest.spyOn(MyClass.prototype, 'methodName');
// Use the following instead if you need not to just spy but also to replace the default method implementation:
// mockedFn = jest.spyOn(MyClass.prototype, 'methodName').mockImplementation(() => {/*custom implementation*/});
});
afterEach(() => {
// Reset to the original method implementation (non-mocked) and clear all the mock data
mockedFn.mockRestore();
});
it('does first thing', () => {
/* Test with the default mock implementation */
});
it('does second thing', () => {
mockedFn.mockImplementation(() => {/*custom implementation just for this test*/});
/* Test utilising this custom mock implementation. It is reset after the test. */
});
it('does third thing', () => {
/* Another test with the default mock implementation */
});
});
I did not manage to define the mock inside the test itself so I discover that I could mock several results for the same service mock like this :
jest.mock("#/services/ApiService", () => {
return {
apiService: {
get: jest.fn()
.mockResolvedValueOnce({response: {value:"Value", label:"Test"}})
.mockResolvedValueOnce(null),
}
};
});
I hope it'll help someone :)
It's a very cool way I've discovered on this blog https://mikeborozdin.com/post/changing-jest-mocks-between-tests/
import { sayHello } from './say-hello';
import * as config from './config';
jest.mock('./config', () => ({
__esModule: true,
CAPITALIZE: null
}));
describe('say-hello', () => {
test('Capitalizes name if config requires that', () => {
config.CAPITALIZE = true;
expect(sayHello('john')).toBe('Hi, John');
});
test('does not capitalize name if config does not require that', () => {
config.CAPITALIZE = false;
expect(sayHello('john')).toBe('Hi, john');
});
});

Categories

Resources