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