diff --git a/frontend/test/__support__/integrated_tests.js b/frontend/test/__support__/integrated_tests.js index b85f0f01b282a7ccaf4015f06f0d531c4996bb38..cfb5fd26b894cf27e877f5b18653495f767f2a72 100644 --- a/frontend/test/__support__/integrated_tests.js +++ b/frontend/test/__support__/integrated_tests.js @@ -20,23 +20,29 @@ import { Provider } from 'react-redux'; import { createMemoryHistory } from 'history' import { getStore } from "metabase/store"; -import { createRoutes, Router, useRouterHistory } from "react-router"; +import { createRoutes, Link, Router, useRouterHistory } from "react-router"; import _ from 'underscore'; +import chalk from "chalk"; // Importing isomorphic-fetch sets the global `fetch` and `Headers` objects that are used here import fetch from 'isomorphic-fetch'; import { refreshSiteSettings } from "metabase/redux/settings"; + import { getRoutes as getNormalRoutes } from "metabase/routes"; import { getRoutes as getPublicRoutes } from "metabase/routes-public"; import { getRoutes as getEmbedRoutes } from "metabase/routes-embed"; +import moment from "moment"; +import Button from "metabase/components/Button"; + let hasStartedCreatingStore = false; let hasFinishedCreatingStore = false let loginSession = null; // Stores the current login session let previousLoginSession = null; let simulateOfflineMode = false; + /** * Login to the Metabase test instance with default credentials */ @@ -168,7 +174,7 @@ export const createTestStore = async ({ publicApp = false, embedApp = false } = const getRoutes = publicApp ? getPublicRoutes : (embedApp ? getEmbedRoutes : getNormalRoutes); const reducers = (publicApp || embedApp) ? publicReducers : normalReducers; const store = getStore(reducers, history, undefined, (createStore) => testStoreEnhancer(createStore, history, getRoutes)); - store.setFinalStoreInstance(store); + store._setFinalStoreInstance(store); if (!publicApp) { await store.dispatch(refreshSiteSettings()); @@ -188,14 +194,15 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { _onActionDispatched: null, _dispatchedActions: [], _finalStoreInstance: null, + _usingAppContainer: false, - setFinalStoreInstance: (finalStore) => { - store._finalStoreInstance = finalStore; - }, dispatch: (action) => { const result = store._originalDispatch(action); - store._dispatchedActions = store._dispatchedActions.concat([action]); + store._dispatchedActions = store._dispatchedActions.concat([{ + ...action, + timestamp: Date.now() + }]); if (store._onActionDispatched) store._onActionDispatched(); return result; }, @@ -211,6 +218,10 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { * Convenient in tests for waiting specific actions to be executed after mounting a React container. */ waitForActions: (actionTypes, {timeout = 8000} = {}) => { + if (store._onActionDispatched) { + return Promise.reject(new Error("You have an earlier `store.waitForActions(...)` still in progress – have you forgotten to prepend `await` to the method call?")) + } + actionTypes = Array.isArray(actionTypes) ? actionTypes : [actionTypes] const allActionsAreTriggered = () => _.every(actionTypes, actionType => @@ -223,7 +234,10 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { } else { return new Promise((resolve, reject) => { store._onActionDispatched = () => { - if (allActionsAreTriggered()) resolve() + if (allActionsAreTriggered()) { + store._onActionDispatched = null; + resolve() + } }; setTimeout(() => { store._onActionDispatched = null; @@ -234,8 +248,10 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { } else { return reject( new Error( - `Actions ${actionTypes.join(", ")} were not dispatched within ${timeout}ms. ` + - `Dispatched actions so far: ${store._dispatchedActions.map((a) => a.type).join(", ")}` + `These actions were not dispatched within ${timeout}ms:\n` + + chalk.cyan(actionTypes.join("\n")) + + "\n\nDispatched actions since initialization / last call of `store.resetDispatchedActions()`:\n" + + (store._dispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") ) ) } @@ -246,7 +262,10 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { }, logDispatchedActions: () => { - console.log(`Dispatched actions so far: ${store._dispatchedActions.map((a) => a.type).join(", ")}`); + console.log( + chalk.bold("\n\nDispatched actions since initialization / last call of `store.resetDispatchedActions()`:\n") + + store._dispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions" + ) }, pushPath: (path) => history.push(path), @@ -256,7 +275,7 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { warnIfStoreCreationNotComplete: () => { if (!hasFinishedCreatingStore) { console.warn( - "Seems that you don't wait until the store creation has completely finished. " + + "Seems that you haven't waited until the store creation has completely finished. " + "This means that site settings might not have been completely loaded. " + "Please add `await` in front of createTestStore call.") } @@ -264,6 +283,7 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { connectContainer: (reactContainer) => { store.warnIfStoreCreationNotComplete(); + store._usingAppContainer = false; const routes = createRoutes(getRoutes(store._finalStoreInstance)) return store._connectWithStore( @@ -277,6 +297,7 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { getAppContainer: () => { store.warnIfStoreCreationNotComplete(); + store._usingAppContainer = true; return store._connectWithStore( <Router history={history}> @@ -285,6 +306,14 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { ) }, + /** For having internally access to the store with all middlewares included **/ + _setFinalStoreInstance: (finalStore) => { + store._finalStoreInstance = finalStore; + }, + + _formatDispatchedAction: (action) => + moment(action.timestamp).format("hh:mm:ss.SSS") + " " + chalk.cyan(action.type), + // eslint-disable-next-line react/display-name _connectWithStore: (reactContainer) => <Provider store={store._finalStoreInstance}> @@ -297,19 +326,39 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { } } -export const clickRouterLink = (linkEnzymeWrapper) => { - // This hits an Enzyme bug so we should find some other way to warn the user :/ - // https://github.com/airbnb/enzyme/pull/769 +export const click = (enzymeWrapper) => { + const nodeType = enzymeWrapper.type(); + if (nodeType === Button || nodeType === "button") { + console.warn( + 'You are calling `click` for a button; you would probably want to use `clickButton` instead as ' + + 'it takes all button click scenarios into account.' + ) + } + // 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 }); +} + +// DEPRECATED +export const clickRouterLink = click + +export const clickButton = (enzymeWrapper) => { + const closestButton = enzymeWrapper.closest("button"); + // const childButton = enzymeWrapper.children("button"); - // if (linkEnzymeWrapper.closest(Router).length === 0) { - // console.warn( - // "Trying to click a link with a component mounted with `store.connectContainer(container)`. Usually " + - // "you want to use `store.getAppContainer()` instead because it has a complete support for react-router." - // ) - // } + if (closestButton.length === 1) { + closestButton.simulate("submit"); // for forms with onSubmit + closestButton.simulate("click"); // for lone buttons / forms without onSubmit + } else { + throw new Error('Couldn\'t find a button element to click in clickButton'); + } +} - linkEnzymeWrapper.simulate('click', {button: 0}); +export const setInputValue = (inputWrapper, value, { blur = true } = {}) => { + inputWrapper.simulate('change', { target: { value: value } }); + if (blur) inputWrapper.simulate("blur") } + // Commonly used question helpers that are temporarily here // TODO Atte Keinänen 6/27/17: Put all metabase-lib -related test helpers to one file export const createSavedQuestion = async (unsavedQuestion) => { diff --git a/frontend/test/admin/databases/DatabaseListApp.integ.spec.js b/frontend/test/admin/databases/DatabaseListApp.integ.spec.js index e2f8305db15781b6d00a986ae30e461ddb4c41b1..e22cc0b4b68a2fe2d685197a7951350c55b18a3a 100644 --- a/frontend/test/admin/databases/DatabaseListApp.integ.spec.js +++ b/frontend/test/admin/databases/DatabaseListApp.integ.spec.js @@ -1,7 +1,9 @@ import { login, createTestStore, - clickRouterLink + click, + clickButton, + setInputValue } from "__support__/integrated_tests"; import { mount } from "enzyme"; @@ -60,7 +62,7 @@ describe('dashboard list', () => { const listAppBeforeAdd = app.find(DatabaseListApp) const addDbButton = listAppBeforeAdd.find('.Button.Button--primary').first() - clickRouterLink(addDbButton) + click(addDbButton) const dbDetailsForm = app.find(DatabaseEditApp); expect(dbDetailsForm.length).toBe(1); @@ -70,7 +72,7 @@ describe('dashboard list', () => { expect(dbDetailsForm.find('button[children="Save"]').props().disabled).toBe(true) const updateInputValue = (name, value) => - dbDetailsForm.find(`input[name="${name}"]`).simulate('change', { target: { value } }); + setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value); updateInputValue("name", "Test db name"); updateInputValue("dbname", "test_postgres_db"); @@ -79,7 +81,7 @@ describe('dashboard list', () => { const saveButton = dbDetailsForm.find('button[children="Save"]') expect(saveButton.props().disabled).toBe(false) - saveButton.simulate("submit"); + clickButton(saveButton) // Now the submit button should be disabled so that you aren't able to trigger the db creation action twice await store.waitForActions([CREATE_DATABASE_STARTED]) @@ -111,7 +113,8 @@ describe('dashboard list', () => { const listAppBeforeAdd = app.find(DatabaseListApp) const addDbButton = listAppBeforeAdd.find('.Button.Button--primary').first() - clickRouterLink(addDbButton) + + click(addDbButton) // ROUTER LINK const dbDetailsForm = app.find(DatabaseEditApp); expect(dbDetailsForm.length).toBe(1); @@ -121,15 +124,17 @@ describe('dashboard list', () => { const saveButton = dbDetailsForm.find('button[children="Save"]') expect(saveButton.props().disabled).toBe(true) + // TODO: Apply change method here const updateInputValue = (name, value) => - dbDetailsForm.find(`input[name="${name}"]`).simulate('change', { target: { value } }); + setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value); updateInputValue("name", "Test db name"); updateInputValue("dbname", "test_postgres_db"); updateInputValue("user", "uberadmin"); + // TODO: Apply button submit thing here expect(saveButton.props().disabled).toBe(false) - saveButton.simulate("submit"); + clickButton(saveButton) await store.waitForActions([CREATE_DATABASE_STARTED]) expect(saveButton.text()).toBe("Saving..."); @@ -155,11 +160,11 @@ describe('dashboard list', () => { const deleteButton = wrapper.find('.Button.Button--danger').first() - deleteButton.simulate('click') + click(deleteButton); const deleteModal = wrapper.find('.test-modal') - deleteModal.find('.Form-input').simulate('change', { target: { value: "DELETE" }}) - deleteModal.find('.Button.Button--danger').simulate('click') + setInputValue(deleteModal.find('.Form-input'), "DELETE") + click(deleteModal.find('.Button.Button--danger')); // test that the modal is gone expect(wrapper.find('.test-modal').length).toEqual(0) @@ -197,12 +202,12 @@ describe('dashboard list', () => { const dbCount = wrapper.find('tr').length const deleteButton = wrapper.find('.Button.Button--danger').first() - - deleteButton.simulate('click') + click(deleteButton) const deleteModal = wrapper.find('.test-modal') - deleteModal.find('.Form-input').simulate('change', { target: { value: "DELETE" }}) - deleteModal.find('.Button.Button--danger').simulate('click') + + setInputValue(deleteModal.find('.Form-input'), "DELETE"); + click(deleteModal.find('.Button.Button--danger')) // test that the modal is gone expect(wrapper.find('.test-modal').length).toEqual(0) @@ -235,7 +240,7 @@ describe('dashboard list', () => { const wrapper = app.find(DatabaseListApp) const sampleDatasetEditLink = wrapper.find('a[children="Sample Dataset"]').first() - clickRouterLink(sampleDatasetEditLink); + click(sampleDatasetEditLink); // ROUTER LINK expect(store.getPath()).toEqual("/admin/databases/1") await store.waitForActions([INITIALIZE_DATABASE]); @@ -246,10 +251,10 @@ describe('dashboard list', () => { const nameField = dbDetailsForm.find(`input[name="name"]`); expect(nameField.props().value).toEqual("Sample Dataset") - nameField.simulate('change', { target: { value: newName } }); + setInputValue(nameField, newName); const saveButton = dbDetailsForm.find('button[children="Save"]') - saveButton.simulate("submit"); + clickButton(saveButton) await store.waitForActions([UPDATE_DATABASE_STARTED]); expect(saveButton.text()).toBe("Saving..."); @@ -286,10 +291,10 @@ describe('dashboard list', () => { const tooLongName = "too long name ".repeat(100); const nameField = dbDetailsForm.find(`input[name="name"]`); - nameField.simulate('change', { target: { value: tooLongName } }); + setInputValue(nameField, tooLongName); const saveButton = dbDetailsForm.find('button[children="Save"]') - saveButton.simulate("submit"); + clickButton(saveButton) await store.waitForActions([UPDATE_DATABASE_STARTED]); expect(saveButton.text()).toBe("Saving..."); diff --git a/frontend/test/admin/datamodel/FieldApp.integ.spec.js b/frontend/test/admin/datamodel/FieldApp.integ.spec.js index b946b36338e4fad393c53e5f6cf45aaf8f998246..a1ef3a61220cafe97a957b96de2e7787e1b77d16 100644 --- a/frontend/test/admin/datamodel/FieldApp.integ.spec.js +++ b/frontend/test/admin/datamodel/FieldApp.integ.spec.js @@ -346,6 +346,7 @@ describe("FieldApp", () => { lastMapping.find(Input).simulate('change', {target: {value: "Extraordinarily awesome"}}); const saveButton = valueRemappingsSection.find(ButtonWithStatus) + // TRY WITH clickButton !!! saveButton.simulate("click"); store.waitForActions([UPDATE_FIELD_VALUES]);