Commit f148847a authored by Savas Vedova's avatar Savas Vedova

Merge branch 'ps-simplify-test-setup' into 'master'

Move unit/integration test setup to shared module

See merge request gitlab-org/gitlab!74432
parents af4001a4 84fbc4d1
...@@ -716,16 +716,19 @@ Jest supports [manual module mocks](https://jestjs.io/docs/manual-mocks) by plac ...@@ -716,16 +716,19 @@ Jest supports [manual module mocks](https://jestjs.io/docs/manual-mocks) by plac
If a manual mock is needed for a `node_modules` package, use the `spec/frontend/__mocks__` folder. Here's an example of If a manual mock is needed for a `node_modules` package, use the `spec/frontend/__mocks__` folder. Here's an example of
a [Jest mock for the package `monaco-editor`](https://gitlab.com/gitlab-org/gitlab/-/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js#L1). a [Jest mock for the package `monaco-editor`](https://gitlab.com/gitlab-org/gitlab/-/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js#L1).
If a manual mock is needed for a CE module, place it in `spec/frontend/mocks/ce`. If a manual mock is needed for a CE module, place the implementation in
`spec/frontend/__helpers__/mocks` and add a line to the `frontend/test_setup`
(or the `frontend/shared_test_setup`) that looks something like:
- Files in `spec/frontend/mocks/ce` mocks the corresponding CE module from `app/assets/javascripts`, mirroring the source module's path. ```javascript
- Example: `spec/frontend/mocks/ce/lib/utils/axios_utils` mocks the module `~/lib/utils/axios_utils`. // "~/lib/utils/axios_utils" is the path to the real module
- We don't support mocking EE modules yet. // "helpers/mocks/axios_utils" is the path to the mocked implementation
- If a mock is found for which a source module doesn't exist, the test suite fails. 'Virtual' mocks, or mocks that don't have a 1-to-1 association with a source module, are not supported yet. jest.mock('~/lib/utils/axios_utils', () => jest.requireActual('helpers/mocks/axios_utils'));
```
#### Manual mock examples #### Manual mock examples
- [`mocks/axios_utils`](https://gitlab.com/gitlab-org/gitlab/-/blob/bd20aeb64c4eed117831556c54b40ff4aee9bfd1/spec/frontend/mocks/ce/lib/utils/axios_utils.js#L1) - - [`__helpers__/mocks/axios_utils`](https://gitlab.com/gitlab-org/gitlab/-/blob/a50edd12b3b1531389624086b6381a042c8143ef/spec/frontend/__helpers__/mocks/axios_utils.js#L1) -
This mock is helpful because we don't want any unmocked requests to pass any tests. Also, we are able to inject some test helpers such as `axios.waitForAll`. This mock is helpful because we don't want any unmocked requests to pass any tests. Also, we are able to inject some test helpers such as `axios.waitForAll`.
- [`__mocks__/mousetrap/index.js`](https://gitlab.com/gitlab-org/gitlab/-/blob/cd4c086d894226445be9d18294a060ba46572435/spec/frontend/__mocks__/mousetrap/index.js#L1) - - [`__mocks__/mousetrap/index.js`](https://gitlab.com/gitlab-org/gitlab/-/blob/cd4c086d894226445be9d18294a060ba46572435/spec/frontend/__mocks__/mousetrap/index.js#L1) -
This mock is helpful because the module itself uses AMD format which webpack understands, but is incompatible with the jest environment. This mock doesn't remove This mock is helpful because the module itself uses AMD format which webpack understands, but is incompatible with the jest environment. This mock doesn't remove
......
...@@ -24,4 +24,5 @@ module.exports = { ...@@ -24,4 +24,5 @@ module.exports = {
'^jh_else_ce_test_helpers(/.*)$': '<rootDir>/jh/spec/frontend_integration/test_helpers$1', '^jh_else_ce_test_helpers(/.*)$': '<rootDir>/jh/spec/frontend_integration/test_helpers$1',
}, },
}), }),
timers: 'real',
}; };
/* Common setup for both unit and integration test environments */
import { config as testUtilsConfig } from '@vue/test-utils';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import 'jquery';
import Translate from '~/vue_shared/translate';
import setWindowLocation from './set_window_location_helper';
import { setGlobalDateToFakeDate } from './fake_date';
import { loadHTMLFixture, setHTMLFixture } from './fixtures';
import { TEST_HOST } from './test_constants';
import customMatchers from './matchers';
import './dom_shims';
import './jquery';
import '~/commons/bootstrap';
// This module has some fairly decent visual test coverage in it's own repository.
jest.mock('@gitlab/favicon-overlay');
process.on('unhandledRejection', global.promiseRejectionHandler);
// Fake the `Date` for the rest of the jest spec runtime environment.
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.use(Translate);
// convenience wrapper for migration from Karma
Object.assign(global, {
loadFixtures: loadHTMLFixture,
setFixtures: setHTMLFixture,
});
const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
// Exclude these jQuery matchers
if (JQUERY_MATCHERS_TO_EXCLUDE.includes(matcherName)) {
return;
}
expect.extend({
[matcherName]: matcherFactory().compare,
});
});
expect.extend(customMatchers);
testUtilsConfig.deprecationWarningHandler = (method, message) => {
const ALLOWED_DEPRECATED_METHODS = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/295679
'finding components with `find` or `get`',
// https://gitlab.com/gitlab-org/gitlab/-/issues/295680
'finding components with `findAll`',
];
if (!ALLOWED_DEPRECATED_METHODS.includes(method)) {
global.console.error(message);
}
};
Object.assign(global, {
requestIdleCallback(cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
});
});
},
cancelIdleCallback(id) {
clearTimeout(id);
},
});
beforeEach(() => {
// make sure that each test actually tests something
// see https://jestjs.io/docs/en/expect#expecthasassertions
expect.hasAssertions();
// Reset the mocked window.location. This ensures tests don't interfere with
// each other, and removes the need to tidy up if it was changed for a given
// test.
setWindowLocation(TEST_HOST);
});
/**
* @module
*
* This module implements auto-injected manual mocks that are cleaner than Jest's approach.
*
* See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html
*/
import fs from 'fs';
import path from 'path';
import readdir from 'readdir-enhanced';
const MAX_DEPTH = 20;
const prefixMap = [
// E.g. the mock ce/foo/bar maps to require path ~/foo/bar
{ mocksRoot: 'ce', requirePrefix: '~' },
// { mocksRoot: 'ee', requirePrefix: 'ee' }, // We'll deal with EE-specific mocks later
// { mocksRoot: 'virtual', requirePrefix: '' }, // We'll deal with virtual mocks later
];
const mockFileFilter = (stats) => stats.isFile() && stats.path.endsWith('.js');
const getMockFiles = (root) => readdir.sync(root, { deep: MAX_DEPTH, filter: mockFileFilter });
// Function that performs setting a mock. This has to be overridden by the unit test, because
// jest.setMock can't be overwritten across files.
// Use require() because jest.setMock expects the CommonJS exports object
const defaultSetMock = (srcPath, mockPath) =>
jest.mock(srcPath, () => jest.requireActual(mockPath));
export const setupManualMocks = function setupManualMocks(setMock = defaultSetMock) {
prefixMap.forEach(({ mocksRoot, requirePrefix }) => {
const mocksRootAbsolute = path.join(__dirname, mocksRoot);
if (!fs.existsSync(mocksRootAbsolute)) {
return;
}
getMockFiles(path.join(__dirname, mocksRoot)).forEach((mockPath) => {
const mockPathNoExt = mockPath.substring(0, mockPath.length - path.extname(mockPath).length);
const sourcePath = path.join(requirePrefix, mockPathNoExt);
const mockPathRelative = `./${path.join(mocksRoot, mockPathNoExt)}`;
try {
setMock(sourcePath, mockPathRelative);
} catch (e) {
if (e.message.includes('Could not locate module')) {
// The corresponding mocked module doesn't exist. Raise a better error.
// Eventualy, we may support virtual mocks (mocks whose path doesn't directly correspond
// to a module, like with the `ee_else_ce` prefix).
throw new Error(
`A manual mock was defined for module ${sourcePath}, but the module doesn't exist!`,
);
}
}
});
});
};
/* eslint-disable global-require */
import path from 'path';
import axios from '~/lib/utils/axios_utils';
const absPath = path.join.bind(null, __dirname);
jest.mock('fs');
jest.mock('readdir-enhanced');
describe('mocks_helper.js', () => {
let setupManualMocks;
const setMock = jest.fn().mockName('setMock');
let fs;
let readdir;
beforeAll(() => {
jest.resetModules();
jest.setMock = jest.fn().mockName('jest.setMock');
fs = require('fs');
readdir = require('readdir-enhanced');
// We need to provide setupManualMocks with a mock function that pretends to do the setup of
// the mock. This is because we can't mock jest.setMock across files.
setupManualMocks = () => require('./mocks_helper').setupManualMocks(setMock);
});
afterEach(() => {
fs.existsSync.mockReset();
readdir.sync.mockReset();
setMock.mockReset();
});
it('enumerates through mock file roots', () => {
setupManualMocks();
expect(fs.existsSync).toHaveBeenCalledTimes(1);
expect(fs.existsSync).toHaveBeenNthCalledWith(1, absPath('ce'));
expect(readdir.sync).toHaveBeenCalledTimes(0);
});
it("doesn't traverse the directory tree infinitely", () => {
fs.existsSync.mockReturnValue(true);
readdir.sync.mockReturnValue([]);
setupManualMocks();
const readdirSpy = readdir.sync;
expect(readdirSpy).toHaveBeenCalled();
readdirSpy.mock.calls.forEach((call) => {
expect(call[1].deep).toBeLessThan(100);
});
});
it('sets up mocks for CE (the ~/ prefix)', () => {
fs.existsSync.mockImplementation((root) => root.endsWith('ce'));
readdir.sync.mockReturnValue(['root.js', 'lib/utils/util.js']);
setupManualMocks();
expect(readdir.sync).toHaveBeenCalledTimes(1);
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
expect(setMock).toHaveBeenCalledTimes(2);
expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root');
expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util');
});
it('sets up mocks for all roots', () => {
const files = {
[absPath('ce')]: ['root', 'lib/utils/util'],
[absPath('node')]: ['jquery', '@babel/core'],
};
fs.existsSync.mockReturnValue(true);
readdir.sync.mockImplementation((root) => files[root]);
setupManualMocks();
expect(readdir.sync).toHaveBeenCalledTimes(1);
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
expect(setMock).toHaveBeenCalledTimes(2);
expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root');
expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util');
});
it('fails when given a virtual mock', () => {
fs.existsSync.mockImplementation((p) => p.endsWith('ce'));
readdir.sync.mockReturnValue(['virtual', 'shouldntBeImported']);
setMock.mockImplementation(() => {
throw new Error('Could not locate module');
});
expect(setupManualMocks).toThrow(
new Error("A manual mock was defined for module ~/virtual, but the module doesn't exist!"),
);
expect(readdir.sync).toHaveBeenCalledTimes(1);
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
});
describe('auto-injection', () => {
it('handles ambiguous paths', () => {
jest.isolateModules(() => {
const axios2 = require('../../../app/assets/javascripts/lib/utils/axios_utils').default;
expect(axios2.isMock).toBe(true);
});
});
it('survives jest.isolateModules()', (done) => {
jest.isolateModules(() => {
const axios2 = require('~/lib/utils/axios_utils').default;
expect(axios2.isMock).toBe(true);
done();
});
});
it('can be unmocked and remocked', () => {
jest.dontMock('~/lib/utils/axios_utils');
jest.resetModules();
const axios2 = require('~/lib/utils/axios_utils').default;
expect(axios2).not.toBe(axios);
expect(axios2.isMock).toBeUndefined();
jest.doMock('~/lib/utils/axios_utils');
jest.resetModules();
const axios3 = require('~/lib/utils/axios_utils').default;
expect(axios3).not.toBe(axios2);
expect(axios3.isMock).toBe(true);
});
});
});
import { config as testUtilsConfig } from '@vue/test-utils'; /* Setup for unit test environment */
import * as jqueryMatchers from 'custom-jquery-matchers'; import 'helpers/shared_test_setup';
import Vue from 'vue'; import { initializeTestTimeout } from 'helpers/timeout';
import 'jquery';
import { setGlobalDateToFakeDate } from 'helpers/fake_date';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import Translate from '~/vue_shared/translate';
import { loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
import { initializeTestTimeout } from './__helpers__/timeout';
import customMatchers from './matchers';
import { setupManualMocks } from './mocks/mocks_helper';
import './__helpers__/dom_shims'; jest.mock('~/lib/utils/axios_utils', () => jest.requireActual('helpers/mocks/axios_utils'));
import './__helpers__/jquery';
import '~/commons/bootstrap';
// This module has some fairly decent visual test coverage in it's own repository. initializeTestTimeout(process.env.CI ? 6000 : 500);
jest.mock('@gitlab/favicon-overlay');
process.on('unhandledRejection', global.promiseRejectionHandler);
setupManualMocks();
// Fake the `Date` for the rest of the jest spec runtime environment.
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
afterEach(() => afterEach(() =>
// give Promises a bit more time so they fail the right test // give Promises a bit more time so they fail the right test
...@@ -33,71 +13,3 @@ afterEach(() => ...@@ -33,71 +13,3 @@ afterEach(() =>
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
}), }),
); );
initializeTestTimeout(process.env.CI ? 6000 : 500);
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.use(Translate);
// convenience wrapper for migration from Karma
Object.assign(global, {
loadFixtures: loadHTMLFixture,
setFixtures: setHTMLFixture,
});
const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
// Exclude these jQuery matchers
if (JQUERY_MATCHERS_TO_EXCLUDE.includes(matcherName)) {
return;
}
expect.extend({
[matcherName]: matcherFactory().compare,
});
});
expect.extend(customMatchers);
testUtilsConfig.deprecationWarningHandler = (method, message) => {
const ALLOWED_DEPRECATED_METHODS = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/295679
'finding components with `find` or `get`',
// https://gitlab.com/gitlab-org/gitlab/-/issues/295680
'finding components with `findAll`',
];
if (!ALLOWED_DEPRECATED_METHODS.includes(method)) {
global.console.error(message);
}
};
Object.assign(global, {
requestIdleCallback(cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
});
});
},
cancelIdleCallback(id) {
clearTimeout(id);
},
});
beforeEach(() => {
// make sure that each test actually tests something
// see https://jestjs.io/docs/en/expect#expecthasassertions
expect.hasAssertions();
// Reset the mocked window.location. This ensures tests don't interfere with
// each other, and removes the need to tidy up if it was changed for a given
// test.
setWindowLocation(TEST_HOST);
});
import '../../../frontend/test_setup'; import 'helpers/shared_test_setup';
import './setup_globals'; import './setup_globals';
import './setup_axios'; import './setup_axios';
import './setup_serializers'; import './setup_serializers';
......
import { setTestTimeout } from 'helpers/timeout'; import { initializeTestTimeout } from 'helpers/timeout';
initializeTestTimeout(process.env.CI ? 20000 : 7000);
beforeEach(() => { beforeEach(() => {
window.gon = { window.gon = {
api_version: 'v4', api_version: 'v4',
relative_url_root: '', relative_url_root: '',
}; };
setTestTimeout(7000);
jest.useRealTimers();
});
afterEach(() => {
jest.useFakeTimers();
}); });
...@@ -3242,11 +3242,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: ...@@ -3242,11 +3242,6 @@ call-bind@^1.0.0, call-bind@^1.0.2:
function-bind "^1.1.1" function-bind "^1.1.1"
get-intrinsic "^1.0.2" get-intrinsic "^1.0.2"
call-me-maybe@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
callsites@^3.0.0: callsites@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
...@@ -6077,11 +6072,6 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0: ...@@ -6077,11 +6072,6 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0:
dependencies: dependencies:
is-glob "^4.0.1" is-glob "^4.0.1"
glob-to-regexp@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
"glob@5 - 7", glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6: "glob@5 - 7", glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6:
version "7.1.7" version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
...@@ -10177,14 +10167,6 @@ readable-stream@~2.0.6: ...@@ -10177,14 +10167,6 @@ readable-stream@~2.0.6:
string_decoder "~0.10.x" string_decoder "~0.10.x"
util-deprecate "~1.0.1" util-deprecate "~1.0.1"
readdir-enhanced@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/readdir-enhanced/-/readdir-enhanced-2.2.4.tgz#773fb8a8de5f645fb13d9403746d490d4facb3e6"
integrity sha512-JQD83C9gAs5B5j2j40qLn/K83HhR8po3bUonebNeuJQUZbbn7q1HxL9kQuPBtxoXkaUpbtEmpFBw5kzyYnnJDA==
dependencies:
call-me-maybe "^1.0.1"
glob-to-regexp "^0.4.0"
readdirp@~3.4.0: readdirp@~3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
......
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