Commit ae0a4c9e authored by Lukas Eipert's avatar Lukas Eipert

Mock observers globally in Jest

As JSDom is missing global mocks for Observers, we currently need to
mock observers manually in any spec that might use them under the hood.

Our existing mocks are only working on _one_ instance of observers each,
so we have refactored them to work on multiple instances as well.

In order to improve the performance on tests where the Observers aren't
tested, we add a NoopObserver to JSDom.

As an aside, JSDom actually implements a MutationObserver, so no need to
mock it!
parent 9aa0cc42
...@@ -44,7 +44,7 @@ export default class LazyLoader { ...@@ -44,7 +44,7 @@ export default class LazyLoader {
startContentObserver() { startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
if (contentNode) { if (contentNode && !this.mutationObserver) {
this.mutationObserver = new MutationObserver(() => this.searchLazyImages()); this.mutationObserver = new MutationObserver(() => this.searchLazyImages());
this.mutationObserver.observe(contentNode, { this.mutationObserver.observe(contentNode, {
......
...@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils'; ...@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import BoardScope from 'ee/boards/components/board_scope.vue'; import BoardScope from 'ee/boards/components/board_scope.vue';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
...@@ -11,7 +10,6 @@ Vue.use(Vuex); ...@@ -11,7 +10,6 @@ Vue.use(Vuex);
describe('BoardScope', () => { describe('BoardScope', () => {
let wrapper; let wrapper;
let store; let store;
useMockIntersectionObserver();
const createStore = () => { const createStore = () => {
return new Vuex.Store({ return new Vuex.Store({
......
...@@ -52,7 +52,7 @@ class MockIntersectionObserver extends MockObserver { ...@@ -52,7 +52,7 @@ class MockIntersectionObserver extends MockObserver {
* const { trigger: triggerMutate } = useMockMutationObserver(); * const { trigger: triggerMutate } = useMockMutationObserver();
* *
* it('test', () => { * it('test', () => {
* trigger(el, { options: { childList: true }, entry: { } }); * triggerMutate(el, { options: { childList: true }, entry: { } });
* }); * });
* }) * })
* ``` * ```
...@@ -60,33 +60,31 @@ class MockIntersectionObserver extends MockObserver { ...@@ -60,33 +60,31 @@ class MockIntersectionObserver extends MockObserver {
* @param {String} key * @param {String} key
*/ */
const useMockObserver = (key, createMock) => { const useMockObserver = (key, createMock) => {
let mockObserver; let mockObservers = [];
let origObserver; let origObserver;
beforeEach(() => { beforeEach(() => {
origObserver = global[key]; origObserver = global[key];
global[key] = jest.fn().mockImplementation((...args) => { global[key] = jest.fn().mockImplementation((...args) => {
mockObserver = createMock(...args); const mockObserver = createMock(...args);
mockObservers.push(mockObserver);
return mockObserver; return mockObserver;
}); });
}); });
afterEach(() => { afterEach(() => {
mockObserver = null; mockObservers.forEach((x) => x.disconnect());
mockObservers = [];
global[key] = origObserver; global[key] = origObserver;
}); });
const trigger = (...args) => { const trigger = (...args) => {
if (!mockObserver) { mockObservers.forEach((observer) => {
return; observer.$_triggerObserve(...args);
} });
mockObserver.$_triggerObserve(...args);
}; };
const observersCount = () => mockObserver.$_observers.length; return { trigger };
return { trigger, observersCount };
}; };
export const useMockIntersectionObserver = () => export const useMockIntersectionObserver = () =>
......
...@@ -6,7 +6,6 @@ import VueApollo from 'vue-apollo'; ...@@ -6,7 +6,6 @@ import VueApollo from 'vue-apollo';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql'; import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql'; import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
...@@ -57,7 +56,6 @@ describe('AlertsSettingsWrapper', () => { ...@@ -57,7 +56,6 @@ describe('AlertsSettingsWrapper', () => {
let wrapper; let wrapper;
let fakeApollo; let fakeApollo;
let destroyIntegrationHandler; let destroyIntegrationHandler;
useMockIntersectionObserver();
const httpMappingData = { const httpMappingData = {
payloadExample: '{"test: : "field"}', payloadExample: '{"test: : "field"}',
......
...@@ -88,13 +88,32 @@ class CustomEnvironment extends JSDOMEnvironment { ...@@ -88,13 +88,32 @@ class CustomEnvironment extends JSDOMEnvironment {
}), }),
}); });
this.global.PerformanceObserver = class { /**
* JSDom doesn't have an own observer implementation, so this a Noop Observer.
* If you are testing functionality, related to observers, have a look at __helpers__/mock_dom_observer.js
*
* JSDom actually implements a _proper_ MutationObserver, so no need to mock it!
*/
class NoopObserver {
/* eslint-disable no-useless-constructor, no-unused-vars, no-empty-function, class-methods-use-this */ /* eslint-disable no-useless-constructor, no-unused-vars, no-empty-function, class-methods-use-this */
constructor(callback) {} constructor(callback) {}
disconnect() {} disconnect() {}
observe(element, initObject) {} observe(element, initObject) {}
unobserve(element) {}
takeRecords() {
return [];
}
/* eslint-enable no-useless-constructor, no-unused-vars, no-empty-function, class-methods-use-this */ /* eslint-enable no-useless-constructor, no-unused-vars, no-empty-function, class-methods-use-this */
}; }
['IntersectionObserver', 'PerformanceObserver', 'ResizeObserver'].forEach((observer) => {
if (this.global[observer]) {
throw new Error(
`We overwrite an existing Observer in jsdom (${observer}), are you sure you want to do that?`,
);
}
this.global[observer] = NoopObserver;
});
} }
async teardown() { async teardown() {
......
...@@ -2,7 +2,6 @@ import { GlIntersectionObserver } from '@gitlab/ui'; ...@@ -2,7 +2,6 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
import IssuableApp from '~/issue_show/components/app.vue'; import IssuableApp from '~/issue_show/components/app.vue';
import DescriptionComponent from '~/issue_show/components/description.vue'; import DescriptionComponent from '~/issue_show/components/description.vue';
...@@ -30,8 +29,6 @@ jest.mock('~/issue_show/event_hub'); ...@@ -30,8 +29,6 @@ jest.mock('~/issue_show/event_hub');
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest]; const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
describe('Issuable output', () => { describe('Issuable output', () => {
useMockIntersectionObserver();
let mock; let mock;
let realtimeRequestCount = 0; let realtimeRequestCount = 0;
let wrapper; let wrapper;
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { initIssuableApp } from '~/issue_show/issue'; import { initIssuableApp } from '~/issue_show/issue';
import * as parseData from '~/issue_show/utils/parse_data'; import * as parseData from '~/issue_show/utils/parse_data';
...@@ -10,8 +9,6 @@ import { appProps } from './mock_data/mock_data'; ...@@ -10,8 +9,6 @@ import { appProps } from './mock_data/mock_data';
const mock = new MockAdapter(axios); const mock = new MockAdapter(axios);
mock.onGet().reply(200); mock.onGet().reply(200);
useMockIntersectionObserver();
jest.mock('~/lib/utils/poll'); jest.mock('~/lib/utils/poll');
const setupHTML = (initialData) => { const setupHTML = (initialData) => {
......
...@@ -4,7 +4,7 @@ import { useMockMutationObserver } from 'helpers/mock_dom_observer'; ...@@ -4,7 +4,7 @@ import { useMockMutationObserver } from 'helpers/mock_dom_observer';
import Popovers from '~/popovers/components/popovers.vue'; import Popovers from '~/popovers/components/popovers.vue';
describe('popovers/components/popovers.vue', () => { describe('popovers/components/popovers.vue', () => {
const { trigger: triggerMutate, observersCount } = useMockMutationObserver(); const { trigger: triggerMutate } = useMockMutationObserver();
let wrapper; let wrapper;
const buildWrapper = (...targets) => { const buildWrapper = (...targets) => {
...@@ -120,10 +120,13 @@ describe('popovers/components/popovers.vue', () => { ...@@ -120,10 +120,13 @@ describe('popovers/components/popovers.vue', () => {
it('disconnects mutation observer on beforeDestroy', async () => { it('disconnects mutation observer on beforeDestroy', async () => {
await buildWrapper(createPopoverTarget()); await buildWrapper(createPopoverTarget());
const { observer } = wrapper.vm;
jest.spyOn(observer, 'disconnect');
expect(observersCount()).toBe(1); expect(observer.disconnect).toHaveBeenCalledTimes(0);
wrapper.destroy(); wrapper.destroy();
expect(observersCount()).toBe(0);
expect(observer.disconnect).toHaveBeenCalledTimes(1);
}); });
}); });
...@@ -4,7 +4,7 @@ import { useMockMutationObserver } from 'helpers/mock_dom_observer'; ...@@ -4,7 +4,7 @@ import { useMockMutationObserver } from 'helpers/mock_dom_observer';
import Tooltips from '~/tooltips/components/tooltips.vue'; import Tooltips from '~/tooltips/components/tooltips.vue';
describe('tooltips/components/tooltips.vue', () => { describe('tooltips/components/tooltips.vue', () => {
const { trigger: triggerMutate, observersCount } = useMockMutationObserver(); const { trigger: triggerMutate } = useMockMutationObserver();
let wrapper; let wrapper;
const buildWrapper = () => { const buildWrapper = () => {
...@@ -211,11 +211,14 @@ describe('tooltips/components/tooltips.vue', () => { ...@@ -211,11 +211,14 @@ describe('tooltips/components/tooltips.vue', () => {
it('disconnects mutation observer on beforeDestroy', () => { it('disconnects mutation observer on beforeDestroy', () => {
buildWrapper(); buildWrapper();
wrapper.vm.addTooltips([createTooltipTarget()]); wrapper.vm.addTooltips([createTooltipTarget()]);
const { observer } = wrapper.vm;
jest.spyOn(observer, 'disconnect');
expect(observersCount()).toBe(1); expect(observer.disconnect).toHaveBeenCalledTimes(0);
wrapper.destroy(); wrapper.destroy();
expect(observersCount()).toBe(0);
expect(observer.disconnect).toHaveBeenCalledTimes(1);
}); });
it('exposes hidden event', async () => { it('exposes hidden event', async () => {
......
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
/** /**
...@@ -7,8 +6,6 @@ import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; ...@@ -7,8 +6,6 @@ import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
* on underlying DOM methods. * on underlying DOM methods.
*/ */
describe('AutofocusOnShow directive', () => { describe('AutofocusOnShow directive', () => {
useMockIntersectionObserver();
describe('with input invisible on component render', () => { describe('with input invisible on component render', () => {
let el; let el;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment