Merge branch 'mh/net-mocks-2' into 'master'
Mockify jquery and axios packages into `spec/frontend` Closes #60136 See merge request gitlab-org/gitlab-ce!29272
This commit is contained in:
commit
76d68e9cee
9 changed files with 269 additions and 14 deletions
|
@ -191,6 +191,7 @@
|
|||
"pixelmatch": "^4.0.2",
|
||||
"postcss": "^7.0.14",
|
||||
"prettier": "1.18.2",
|
||||
"readdir-enhanced": "^2.2.4",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylelint-scss": "^3.5.4",
|
||||
|
|
15
spec/frontend/mocks/ce/lib/utils/axios_utils.js
Normal file
15
spec/frontend/mocks/ce/lib/utils/axios_utils.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const axios = jest.requireActual('~/lib/utils/axios_utils').default;
|
||||
|
||||
axios.isMock = true;
|
||||
|
||||
// Fail tests for unmocked requests
|
||||
axios.defaults.adapter = config => {
|
||||
const message =
|
||||
`Unexpected unmocked request: ${JSON.stringify(config, null, 2)}\n` +
|
||||
'Consider using the `axios-mock-adapter` in tests.';
|
||||
const error = new Error(message);
|
||||
error.config = config;
|
||||
throw error;
|
||||
};
|
||||
|
||||
export default axios;
|
60
spec/frontend/mocks/mocks_helper.js
Normal file
60
spec/frontend/mocks/mocks_helper.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* @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: 'node', requirePrefix: '' },
|
||||
// { 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));
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
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!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
147
spec/frontend/mocks/mocks_helper_spec.js
Normal file
147
spec/frontend/mocks/mocks_helper_spec.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
/* eslint-disable global-require, promise/catch-or-return */
|
||||
|
||||
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(2);
|
||||
expect(fs.existsSync).toHaveBeenNthCalledWith(1, absPath('ce'));
|
||||
expect(fs.existsSync).toHaveBeenNthCalledWith(2, absPath('node'));
|
||||
|
||||
expect(readdir.sync).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("doesn't traverse the directory tree infinitely", () => {
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
readdir.sync.mockReturnValue([]);
|
||||
setupManualMocks();
|
||||
|
||||
readdir.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 node_modules', () => {
|
||||
fs.existsSync.mockImplementation(root => root.endsWith('node'));
|
||||
readdir.sync.mockReturnValue(['jquery', '@babel/core']);
|
||||
setupManualMocks();
|
||||
|
||||
expect(readdir.sync).toHaveBeenCalledTimes(1);
|
||||
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('node'));
|
||||
|
||||
expect(setMock).toHaveBeenCalledTimes(2);
|
||||
expect(setMock).toHaveBeenNthCalledWith(1, 'jquery', './node/jquery');
|
||||
expect(setMock).toHaveBeenNthCalledWith(2, '@babel/core', './node/@babel/core');
|
||||
});
|
||||
|
||||
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(2);
|
||||
expect(readdir.sync.mock.calls[0][0]).toBe(absPath('ce'));
|
||||
expect(readdir.sync.mock.calls[1][0]).toBe(absPath('node'));
|
||||
|
||||
expect(setMock).toHaveBeenCalledTimes(4);
|
||||
expect(setMock).toHaveBeenNthCalledWith(1, '~/root', './ce/root');
|
||||
expect(setMock).toHaveBeenNthCalledWith(2, '~/lib/utils/util', './ce/lib/utils/util');
|
||||
expect(setMock).toHaveBeenNthCalledWith(3, 'jquery', './node/jquery');
|
||||
expect(setMock).toHaveBeenNthCalledWith(4, '@babel/core', './node/@babel/core');
|
||||
});
|
||||
|
||||
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.get('http://gitlab.com'))
|
||||
.rejects.toThrow('Unexpected unmocked request')
|
||||
.then(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);
|
||||
});
|
||||
});
|
||||
});
|
13
spec/frontend/mocks/node/jquery.js
vendored
Normal file
13
spec/frontend/mocks/node/jquery.js
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable import/no-commonjs */
|
||||
|
||||
const $ = jest.requireActual('jquery');
|
||||
|
||||
// Fail tests for unmocked requests
|
||||
$.ajax = () => {
|
||||
throw new Error(
|
||||
'Unexpected unmocked jQuery.ajax() call! Make sure to mock jQuery.ajax() in tests.',
|
||||
);
|
||||
};
|
||||
|
||||
// jquery is not an ES6 module
|
||||
module.exports = $;
|
13
spec/frontend/mocks_spec.js
Normal file
13
spec/frontend/mocks_spec.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import $ from 'jquery';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
describe('Mock auto-injection', () => {
|
||||
describe('mocks', () => {
|
||||
it('~/lib/utils/axios_utils', () =>
|
||||
expect(axios.get('http://gitlab.com')).rejects.toThrow('Unexpected unmocked request'));
|
||||
|
||||
it('jQuery.ajax()', () => {
|
||||
expect($.ajax).toThrow('Unexpected unmocked');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,7 +7,6 @@ import { refreshCurrentPage } from '~/lib/utils/url_utility';
|
|||
import createFlash from '~/flash';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
|
||||
jest.mock('~/lib/utils/axios_utils');
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
jest.mock('~/flash');
|
||||
|
||||
|
@ -32,6 +31,10 @@ describe('operation settings external dashboard component', () => {
|
|||
wrapper = shallow ? shallowMount(...config) : mount(...config);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(axios, 'patch').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper.destroy) {
|
||||
wrapper.destroy();
|
||||
|
|
|
@ -2,10 +2,10 @@ import Vue from 'vue';
|
|||
import * as jqueryMatchers from 'custom-jquery-matchers';
|
||||
import $ from 'jquery';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { config as testUtilsConfig } from '@vue/test-utils';
|
||||
import { initializeTestTimeout } from './helpers/timeout';
|
||||
import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
|
||||
import { setupManualMocks } from './mocks/mocks_helper';
|
||||
|
||||
// Expose jQuery so specs using jQuery plugins can be imported nicely.
|
||||
// Here is an issue to explore better alternatives:
|
||||
|
@ -14,6 +14,8 @@ window.jQuery = $;
|
|||
|
||||
process.on('unhandledRejection', global.promiseRejectionHandler);
|
||||
|
||||
setupManualMocks();
|
||||
|
||||
afterEach(() =>
|
||||
// give Promises a bit more time so they fail the right test
|
||||
new Promise(setImmediate).then(() => {
|
||||
|
@ -24,18 +26,6 @@ afterEach(() =>
|
|||
|
||||
initializeTestTimeout(process.env.CI ? 5000 : 500);
|
||||
|
||||
// fail tests for unmocked requests
|
||||
beforeEach(done => {
|
||||
axios.defaults.adapter = config => {
|
||||
const error = new Error(`Unexpected unmocked request: ${JSON.stringify(config, null, 2)}`);
|
||||
error.config = config;
|
||||
done.fail(error);
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
Vue.config.devtools = false;
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -4919,6 +4919,11 @@ glob-to-regexp@^0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
|
||||
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
|
||||
|
||||
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.1:
|
||||
version "7.1.4"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
|
||||
|
@ -9099,6 +9104,14 @@ readable-stream@~2.0.6:
|
|||
string_decoder "~0.10.x"
|
||||
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@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
|
||||
|
|
Loading…
Reference in a new issue