From b2356dca1a0bb6e7b13340ba8a10b10f8fe5bac2 Mon Sep 17 00:00:00 2001 From: Paul Slaughter <pslaughter@gitlab.com> Date: Wed, 7 Apr 2021 12:13:25 -0500 Subject: [PATCH] Setup fake web worker for jest - This does a fake implementation of some trivial web worker API. This way we can have integration specs without overly stubbing these out. --- jest.config.base.js | 3 + spec/frontend/__helpers__/web_worker_fake.js | 71 +++++++++++++++++++ spec/frontend/__helpers__/web_worker_mock.js | 10 --- .../__helpers__/web_worker_transformer.js | 18 +++++ spec/frontend/diffs/store/actions_spec.js | 2 + .../mocks/ce/diffs/workers/tree_worker.js | 1 - .../mocks/ce/ide/lib/diff/diff_worker.js | 1 - 7 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 spec/frontend/__helpers__/web_worker_fake.js delete mode 100644 spec/frontend/__helpers__/web_worker_mock.js create mode 100644 spec/frontend/__helpers__/web_worker_transformer.js delete mode 100644 spec/frontend/mocks/ce/diffs/workers/tree_worker.js delete mode 100644 spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js diff --git a/jest.config.base.js b/jest.config.base.js index 52e29339e559..ef7802ff724a 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -86,6 +86,8 @@ module.exports = (path, options = {}) => { collectCoverageFrom, coverageDirectory: coverageDirectory(), coverageReporters: ['json', 'lcov', 'text-summary', 'clover'], + // We need ignore _worker code coverage since we are manually transforming it + coveragePathIgnorePatterns: ['<rootDir>/node_modules/', '_worker\\.js$'], cacheDirectory: '<rootDir>/tmp/cache/jest', modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'], reporters, @@ -93,6 +95,7 @@ module.exports = (path, options = {}) => { restoreMocks: true, transform: { '^.+\\.(gql|graphql)$': 'jest-transform-graphql', + '^.+_worker\\.js$': './spec/frontend/__helpers__/web_worker_transformer.js', '^.+\\.js$': 'babel-jest', '^.+\\.vue$': 'vue-jest', '^.+\\.(md|zip|png)$': 'jest-raw-loader', diff --git a/spec/frontend/__helpers__/web_worker_fake.js b/spec/frontend/__helpers__/web_worker_fake.js new file mode 100644 index 000000000000..041a9bd8540f --- /dev/null +++ b/spec/frontend/__helpers__/web_worker_fake.js @@ -0,0 +1,71 @@ +import path from 'path'; + +const isRelative = (pathArg) => pathArg.startsWith('.'); + +const transformRequirePath = (base, pathArg) => { + if (!isRelative(pathArg)) { + return pathArg; + } + + return path.resolve(base, pathArg); +}; + +const createRelativeRequire = (filename) => { + const rel = path.relative(__dirname, path.dirname(filename)); + const base = path.resolve(__dirname, rel); + + // reason: Dynamic require should be fine here since the code is dynamically evaluated anyways. + // eslint-disable-next-line import/no-dynamic-require, global-require + return (pathArg) => require(transformRequirePath(base, pathArg)); +}; + +/** + * Simulates a WebWorker module similar to the kind created by Webpack's [`worker-loader`][1] + * + * [1]: https://webpack.js.org/loaders/worker-loader/ + */ +export class FakeWebWorker { + /** + * Constructs a new FakeWebWorker instance + * + * @param {String} filename is the full path of the code, which is used to resolve relative imports. + * @param {String} code is the raw code of the web worker, which is dynamically evaluated on construction. + */ + constructor(filename, code) { + let isAlive = true; + + const clientTarget = new EventTarget(); + const workerTarget = new EventTarget(); + + this.addEventListener = (...args) => clientTarget.addEventListener(...args); + this.removeEventListener = (...args) => clientTarget.removeEventListener(...args); + this.postMessage = (message) => { + if (!isAlive) { + return; + } + + workerTarget.dispatchEvent(new MessageEvent('message', { data: message })); + }; + this.terminate = () => { + isAlive = false; + }; + + const workerScope = { + addEventListener: (...args) => workerTarget.addEventListener(...args), + removeEventListener: (...args) => workerTarget.removeEventListener(...args), + postMessage: (message) => { + if (!isAlive) { + return; + } + + clientTarget.dispatchEvent(new MessageEvent('message', { data: message })); + }, + }; + + // reason: `no-new-func` is like `eval` except it only executed on global scope and it's easy + // to pass in local references. `eval` is very unsafe in production, but in our test environment + // we shold be fine. + // eslint-disable-next-line no-new-func + Function('self', 'require', code)(workerScope, createRelativeRequire(filename)); + } +} diff --git a/spec/frontend/__helpers__/web_worker_mock.js b/spec/frontend/__helpers__/web_worker_mock.js deleted file mode 100644 index 2b4a391e1d2c..000000000000 --- a/spec/frontend/__helpers__/web_worker_mock.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable class-methods-use-this */ -export default class WebWorkerMock { - addEventListener() {} - - removeEventListener() {} - - terminate() {} - - postMessage() {} -} diff --git a/spec/frontend/__helpers__/web_worker_transformer.js b/spec/frontend/__helpers__/web_worker_transformer.js new file mode 100644 index 000000000000..5b2f7d779477 --- /dev/null +++ b/spec/frontend/__helpers__/web_worker_transformer.js @@ -0,0 +1,18 @@ +/* eslint-disable import/no-commonjs */ +const babelJestTransformer = require('babel-jest'); + +// This Jest will transform the code of a WebWorker module into a FakeWebWorker subclass. +// This is meant to mirror Webpack's [`worker-loader`][1]. +// [1]: https://webpack.js.org/loaders/worker-loader/ +module.exports = { + process: (contentArg, filename, ...args) => { + const { code: content } = babelJestTransformer.process(contentArg, filename, ...args); + + return `const { FakeWebWorker } = require("helpers/web_worker_fake"); + module.exports = class JestTransformedWorker extends FakeWebWorker { + constructor() { + super(${JSON.stringify(filename)}, ${JSON.stringify(content)}); + } + };`; + }, +}; diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 0d3eaa15f1e5..822d73506c10 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -258,6 +258,8 @@ describe('DiffsStoreActions', () => { { type: types.SET_LOADING, payload: false }, { type: types.SET_MERGE_REQUEST_DIFFS, payload: diffMetadata.merge_request_diffs }, { type: types.SET_DIFF_METADATA, payload: noFilesData }, + // Workers are synchronous in Jest environment (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58805) + { type: types.SET_TREE_DATA, payload: utils.generateTreeList(diffMetadata.diff_files) }, ], [], () => { diff --git a/spec/frontend/mocks/ce/diffs/workers/tree_worker.js b/spec/frontend/mocks/ce/diffs/workers/tree_worker.js deleted file mode 100644 index 5532a22f8e6f..000000000000 --- a/spec/frontend/mocks/ce/diffs/workers/tree_worker.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'helpers/web_worker_mock'; diff --git a/spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js b/spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js deleted file mode 100644 index 5532a22f8e6f..000000000000 --- a/spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'helpers/web_worker_mock'; -- GitLab