diff --git a/frontend/test/__support__/integrated_tests.js b/frontend/test/__support__/integrated_tests.js index 712b26dd9974072427f6950c26486178978d3117..1ba351c7db2b51604a6031179b7b56e91c3fb8e8 100644 --- a/frontend/test/__support__/integrated_tests.js +++ b/frontend/test/__support__/integrated_tests.js @@ -41,7 +41,6 @@ let loginSession = null; // Stores the current login session let previousLoginSession = null; let simulateOfflineMode = false; - /** * Login to the Metabase test instance with default credentials */ @@ -66,6 +65,9 @@ export function logout() { loginSession = null } +/** + * Lets you recover the previous login session after calling logout + */ export function restorePreviousLogin() { if (previousLoginSession) { loginSession = previousLoginSession @@ -75,7 +77,7 @@ export function restorePreviousLogin() { } /** - * Calls the provided function while simulating that the browser is offline. + * Calls the provided function while simulating that the browser is offline */ export async function whenOffline(callWhenOffline) { simulateOfflineMode = true; @@ -90,81 +92,14 @@ export async function whenOffline(callWhenOffline) { }); } -// Patches the metabase/lib/api module so that all API queries contain the login credential cookie. -// Needed because we are not in a real web browser environment. -api._makeRequest = async (method, url, headers, requestBody, data, options) => { - const headersWithSessionCookie = { - ...headers, - ...(loginSession ? {"Cookie": `${METABASE_SESSION_COOKIE}=${loginSession.id}`} : {}) - } - - const fetchOptions = { - credentials: "include", - method, - headers: new Headers(headersWithSessionCookie), - ...(requestBody ? { body: requestBody } : {}) - }; - - let isCancelled = false - if (options.cancelled) { - options.cancelled.then(() => { - isCancelled = true; - }); - } - const result = simulateOfflineMode - ? { status: 0, responseText: '' } - : (await fetch(api.basename + url, fetchOptions)); - - if (isCancelled) { - throw { status: 0, data: '', isCancelled: true} - } - - let resultBody = null - try { - resultBody = await result.text(); - // Even if the result conversion to JSON fails, we still return the original text - // This is 1-to-1 with the real _makeRequest implementation - resultBody = JSON.parse(resultBody); - } catch (e) {} - - - if (result.status >= 200 && result.status <= 299) { - if (options.transformResponse) { - return options.transformResponse(resultBody, { data }); - } else { - return resultBody - } - } else { - const error = { status: result.status, data: resultBody, isCancelled: false } - if (!simulateOfflineMode) { - console.log('A request made in a test failed with the following error:'); - console.log(error, { depth: null }); - console.log(`The original request: ${method} ${url}`); - if (requestBody) console.log(`Original payload: ${requestBody}`); - } - throw error - } -} - -// Set the correct base url to metabase/lib/api module -if (process.env.E2E_HOST) { - api.basename = process.env.E2E_HOST; -} else { - console.log( - 'Please use `yarn run test-integrated` or `yarn run test-integrated-watch` for running integration tests.' - ) - process.quit(0) -} - /** * Creates an augmented Redux store for testing the whole app including browser history manipulation. Includes: * - A simulated browser history that is used by react-router * - Methods for - * * manipulating the browser history + * * manipulating the simulated browser history * * waiting until specific Redux actions have been dispatched * * getting a React container subtree for the current route */ - export const createTestStore = async ({ publicApp = false, embedApp = false } = {}) => { hasFinishedCreatingStore = false; hasStartedCreatingStore = true; @@ -195,6 +130,9 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { _latestDispatchedActions: [], _finalStoreInstance: null, + /** + * Redux dispatch method middleware that records all dispatched actions + */ dispatch: (action) => { const result = store._originalDispatch(action); @@ -234,42 +172,40 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { if (allActionsAreTriggered()) { // Short-circuit if all action types are already in the history of dispatched actions - return; + return Promise.resolve(); } else { return new Promise((resolve, reject) => { + const timeoutID = setTimeout(() => { + store._onActionDispatched = null; + + return reject( + new Error( + `All these actions were not dispatched within ${timeout}ms:\n` + + chalk.cyan(actionTypes.join("\n")) + + "\n\nDispatched actions since the last call of `waitForActions`:\n" + + (store._latestDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") + + "\n\nDispatched actions since the initialization of test suite:\n" + + (store._allDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") + ) + ) + }, timeout) + store._onActionDispatched = () => { if (allActionsAreTriggered()) { store._latestDispatchedActions = getRemainingActions(); store._onActionDispatched = null; + clearTimeout(timeoutID); resolve() } }; - setTimeout(() => { - store._onActionDispatched = null; - - if (allActionsAreTriggered()) { - // TODO: Figure out why we sometimes end up here instead of _onActionDispatched hook - store._latestDispatchedActions = getRemainingActions(); - resolve() - } else { - return reject( - new Error( - `These actions were not dispatched within ${timeout}ms:\n` + - chalk.cyan(actionTypes.join("\n")) + - "\n\nDispatched actions since the last call of `waitForActions`:\n" + - (store._latestDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") + - "\n\nDispatched actions since the initialization of test suite:\n" + - (store._allDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") - ) - ) - } - - }, timeout) }); } }, - logDispatchedActions: () => { + /** + * Logs the actions that have been dispatched so far + */ + debug: () => { console.log( chalk.bold("Dispatched actions since last call of `waitForActions`:\n") + (store._latestDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") + @@ -278,6 +214,9 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { ) }, + /** + * Methods for manipulating the simulated browser history + */ pushPath: (path) => history.push(path), goBack: () => history.goBack(), getPath: () => urlFormat(history.getCurrentLocation()), @@ -291,6 +230,12 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { } }, + /** + * For testing an individual component that is rendered to the router context. + * The component will receive the same router props as it would if it was part of the complete app component tree. + * + * This is usually a lot faster than `getAppContainer` but doesn't work well with react-router links. + */ connectContainer: (reactContainer) => { store.warnIfStoreCreationNotComplete(); @@ -304,6 +249,10 @@ const testStoreEnhancer = (createStore, history, getRoutes) => { ); }, + /** + * Renders the whole app tree. + * Useful if you want to navigate between different sections of your app in your tests. + */ getAppContainer: () => { store.warnIfStoreCreationNotComplete(); @@ -343,4 +292,70 @@ export const createSavedQuestion = async (unsavedQuestion) => { return savedQuestion } +// Patches the metabase/lib/api module so that all API queries contain the login credential cookie. +// Needed because we are not in a real web browser environment. +api._makeRequest = async (method, url, headers, requestBody, data, options) => { + const headersWithSessionCookie = { + ...headers, + ...(loginSession ? {"Cookie": `${METABASE_SESSION_COOKIE}=${loginSession.id}`} : {}) + } + + const fetchOptions = { + credentials: "include", + method, + headers: new Headers(headersWithSessionCookie), + ...(requestBody ? { body: requestBody } : {}) + }; + + let isCancelled = false + if (options.cancelled) { + options.cancelled.then(() => { + isCancelled = true; + }); + } + const result = simulateOfflineMode + ? { status: 0, responseText: '' } + : (await fetch(api.basename + url, fetchOptions)); + + if (isCancelled) { + throw { status: 0, data: '', isCancelled: true} + } + + let resultBody = null + try { + resultBody = await result.text(); + // Even if the result conversion to JSON fails, we still return the original text + // This is 1-to-1 with the real _makeRequest implementation + resultBody = JSON.parse(resultBody); + } catch (e) {} + + + if (result.status >= 200 && result.status <= 299) { + if (options.transformResponse) { + return options.transformResponse(resultBody, { data }); + } else { + return resultBody + } + } else { + const error = { status: result.status, data: resultBody, isCancelled: false } + if (!simulateOfflineMode) { + console.log('A request made in a test failed with the following error:'); + console.log(error, { depth: null }); + console.log(`The original request: ${method} ${url}`); + if (requestBody) console.log(`Original payload: ${requestBody}`); + } + throw error + } +} + +// Set the correct base url to metabase/lib/api module +if (process.env.E2E_HOST) { + api.basename = process.env.E2E_HOST; +} else { + console.log( + 'Please use `yarn run test-integrated` or `yarn run test-integrated-watch` for running integration tests.' + ) + process.quit(0) +} + jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; diff --git a/frontend/test/admin/settings/settings.integ.spec.js b/frontend/test/admin/settings/settings.integ.spec.js index 7b7d8d1d88a5827dc31c3e0cb2ddd0eb0d45d6f3..b7be2fba0b908d5af379b3e3e7580c0140cd4925 100644 --- a/frontend/test/admin/settings/settings.integ.spec.js +++ b/frontend/test/admin/settings/settings.integ.spec.js @@ -33,7 +33,6 @@ describe("admin/settings", () => { // clear the site name input, send the keys corresponding to the site name, then blur to trigger the update setInputValue(input, siteName) - input.simulate('blur') await store.waitForActions([UPDATE_SETTING]) });