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