From 7cf89a167f07ff222d192008fc1007d2db5b4200 Mon Sep 17 00:00:00 2001
From: Tom Robinson <tlrobinson@gmail.com>
Date: Thu, 7 Nov 2019 13:58:25 -0800
Subject: [PATCH] e2e test improvements (#11277)

---
 frontend/test/__support__/e2e_tests.js        |  9 ++
 frontend/test/__support__/enzyme_utils.js     | 93 ++++++++++++++-----
 .../test/__support__/integration_tests.js     |  7 +-
 .../admin/datamodel/datamodel.e2e.spec.js     |  9 +-
 package.json                                  |  1 +
 yarn.lock                                     |  5 +
 6 files changed, 89 insertions(+), 35 deletions(-)

diff --git a/frontend/test/__support__/e2e_tests.js b/frontend/test/__support__/e2e_tests.js
index 143c7ae46c0..6d85a81f30d 100644
--- a/frontend/test/__support__/e2e_tests.js
+++ b/frontend/test/__support__/e2e_tests.js
@@ -728,3 +728,12 @@ if (
 }
 
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
+
+import { mount } from "./enzyme_utils";
+
+export async function mountApp(url = "/") {
+  const store = await createTestStore();
+  store.pushPath(url);
+  const app = mount(store.getAppContainer());
+  return { app, store };
+}
diff --git a/frontend/test/__support__/enzyme_utils.js b/frontend/test/__support__/enzyme_utils.js
index 00e27f285c2..e5816c3f971 100644
--- a/frontend/test/__support__/enzyme_utils.js
+++ b/frontend/test/__support__/enzyme_utils.js
@@ -1,9 +1,16 @@
 // This must be before all other imports
 import { eventListeners } from "./mocks";
 
+import { ReactWrapper } from "enzyme";
+import proxymise from "proxymise";
+
 import { delay } from "metabase/lib/promise";
 import Button from "metabase/components/Button";
 
+// convienence
+export { mount } from "enzyme";
+export { delay } from "metabase/lib/promise";
+
 // Triggers events that are being listened to with `window.addEventListener` or `document.addEventListener`
 export const dispatchBrowserEvent = (eventName, ...args) => {
   if (eventListeners[eventName]) {
@@ -32,6 +39,9 @@ export const click = enzymeWrapper => {
   // Normal click event. Works for both `onClick` React event handlers and react-router <Link> objects.
   // We simulate a left button click with `{ button: 0 }` because react-router requires that.
   enzymeWrapper.simulate("click", { button: 0 });
+
+  // add a slight delay for robustness
+  return delay(10);
 };
 
 export const clickButton = enzymeWrapper => {
@@ -58,6 +68,9 @@ export const clickButton = enzymeWrapper => {
       enzymeWrapper.simulate("click", { button: 0 });
     } catch (e) {}
   }
+
+  // add a slight delay for robustness
+  return delay(10);
 };
 
 export const setInputValue = (inputWrapper, value, { blur = true } = {}) => {
@@ -85,7 +98,7 @@ export const chooseSelectOption = optionWrapper => {
   parentSelect.simulate("change", { target: { value: optionValue } });
 };
 
-const TIMEOUT = 1000;
+const TIMEOUT = 15000;
 
 async function eventually(fn, timeout = TIMEOUT) {
   const start = Date.now();
@@ -103,29 +116,8 @@ async function eventually(fn, timeout = TIMEOUT) {
   }
 }
 
-export function enhanceEnzymeWrapper(wrapper) {
-  // add a "async" namespace that wraps functions in `eventually`
-  wrapper.async = {
-    find: selector =>
-      eventually(() => {
-        const node = wrapper.find(selector);
-        if (node.exists()) {
-          return node;
-        } else {
-          throw new Error("Not found: " + selector);
-        }
-      }),
-  };
-  return wrapper;
-}
-
 export async function getFormValues(wrapper) {
   const values = {};
-  // enhance the wrapper if it hasn't already been enhanced so we can
-  // pass things other than the top level wrapper from mountStore
-  if (!wrapper.async) {
-    enhanceEnzymeWrapper(wrapper);
-  }
   const inputs = await wrapper.async.find("input");
   for (const input of inputs) {
     values[input.props.name] = input.props.value;
@@ -155,3 +147,60 @@ export async function fillAndSubmitForm(wrapper, values) {
   await fillFormValues(wrapper, values);
   submitForm(wrapper);
 }
+
+function findByText(wrapper, text) {
+  return wrapper.find(`[children=${JSON.stringify(text)}]`);
+}
+function findByIcon(wrapper, icon) {
+  return wrapper.find(`.Icon-${icon}`);
+}
+
+// adds helper  methods to enzyme ReactWrapper
+addReactWrapperMethod("click", click);
+addReactWrapperMethod("clickButton", clickButton);
+addReactWrapperMethod("findByText", findByText);
+addReactWrapperMethod("findByIcon", findByIcon);
+addReactWrapperMethod("setInputValue", setInputValue);
+
+// creates the magic "async" namespace for `find` methods
+Object.defineProperty(ReactWrapper.prototype, "async", {
+  get() {
+    if (!this.__async) {
+      this.__async = {
+        find: asyncFind(this, "find"),
+        findWhere: asyncFind(this, "findWhere"),
+        findByText: asyncFind(this, "findByText"),
+        findByIcon: asyncFind(this, "findByIcon"),
+      };
+    }
+    return this.__async;
+  },
+});
+
+function addReactWrapperMethod(name, method) {
+  ReactWrapper.prototype[name] = function(...args) {
+    return method(this, ...args);
+  };
+}
+
+function asyncFind(wrapper, name) {
+  return (...args) =>
+    // proxymise allows chaining like app.async.find(...).click()
+    proxymise(
+      eventually(() => {
+        const node = wrapper[name](...args);
+        if (node.exists()) {
+          if (node.length > 1) {
+            console.warn(
+              `Found ${node.length} nodes: ${name}(${args.join(", ")})`,
+            );
+          }
+          return node;
+        } else {
+          throw new Error(
+            `Not found within timeout: ${name}(${args.join(", ")})`,
+          );
+        }
+      }),
+    );
+}
diff --git a/frontend/test/__support__/integration_tests.js b/frontend/test/__support__/integration_tests.js
index 91e21e482c8..b10ab2b18ec 100644
--- a/frontend/test/__support__/integration_tests.js
+++ b/frontend/test/__support__/integration_tests.js
@@ -1,7 +1,6 @@
 import React from "react";
-
 import { Provider } from "react-redux";
-import { mount } from "enzyme";
+import { mount } from "__support__/enzyme_utils";
 
 import reducers from "metabase/reducers-main";
 import { getStore } from "metabase/store";
@@ -11,8 +10,6 @@ export { delay } from "metabase/lib/promise";
 
 import { MockResponse, MockRequest } from "xhr-mock";
 
-import { enhanceEnzymeWrapper } from "__support__/enzyme_utils";
-
 // helper for JSON responses, also defaults to 200 status code
 MockResponse.prototype.json = function(object) {
   return this.status(this._status || 200)
@@ -54,8 +51,6 @@ export function mountWithStore(element) {
 
   const wrapper = mount(<Provider store={store}>{element}</Provider>);
 
-  enhanceEnzymeWrapper(wrapper);
-
   // NOTE: automatically call wrapper.update when the store changes:
   // https://github.com/airbnb/enzyme/blob/ed5848085051ac7afef64a7d045d53b1153a8fe7/docs/guides/migration-from-2-to-3.md#for-mount-updates-are-sometimes-required-when-they-werent-before
   store.subscribe(() => wrapper.update());
diff --git a/frontend/test/metabase/admin/datamodel/datamodel.e2e.spec.js b/frontend/test/metabase/admin/datamodel/datamodel.e2e.spec.js
index 6376dbbcca3..95e6cc0229a 100644
--- a/frontend/test/metabase/admin/datamodel/datamodel.e2e.spec.js
+++ b/frontend/test/metabase/admin/datamodel/datamodel.e2e.spec.js
@@ -5,12 +5,7 @@ import {
   deleteAllSegments,
   deleteAllMetrics,
 } from "__support__/e2e_tests";
-import {
-  click,
-  clickButton,
-  setInputValue,
-  enhanceEnzymeWrapper,
-} from "__support__/enzyme_utils";
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 
 import { mount } from "enzyme";
 import { UPDATE_PREVIEW_SUMMARY } from "metabase/admin/datamodel/datamodel";
@@ -42,7 +37,7 @@ describe("admin/datamodel", () => {
       const store = await createTestStore();
 
       store.pushPath("/admin/datamodel/database");
-      const app = enhanceEnzymeWrapper(mount(store.getAppContainer()));
+      const app = mount(store.getAppContainer());
       await store.waitForActions([Tables.actions.fetchList]);
 
       // Open "Orders" table section
diff --git a/package.json b/package.json
index ce2c529626b..0466b3ed77a 100644
--- a/package.json
+++ b/package.json
@@ -158,6 +158,7 @@
     "postcss-url": "^6.0.4",
     "prettier": "1.18.2",
     "promise-loader": "^1.0.0",
+    "proxymise": "^1.0.2",
     "raf": "^3.4.0",
     "react-test-renderer": "15",
     "sauce-connect-launcher": "^1.1.1",
diff --git a/yarn.lock b/yarn.lock
index e014fc69564..88422ad65f7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10664,6 +10664,11 @@ proxy-addr@~2.0.2:
     forwarded "~0.1.2"
     ipaddr.js "1.5.2"
 
+proxymise@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/proxymise/-/proxymise-1.0.2.tgz#34a183fb9dba29b25d5f02a25edaf3b2b71161dd"
+  integrity sha512-ulIs+1o/y6o1B9wJiHT/SIy3QAO5HjxbikdWWrtd3pkqlC3IBcR69z7y3U2ycbbUZeQijQkgXLTqBSiIMK/3xA==
+
 prr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
-- 
GitLab