Skip to content
Snippets Groups Projects
Commit ac1e19a4 authored by Atte Keinänen's avatar Atte Keinänen
Browse files

Formatted store errors, better link/input helpers

parent ad2046dc
Branches
Tags
No related merge requests found
......@@ -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) => {
......
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...");
......
......@@ -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]);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment