From 3e08a935851cb7e5930788743b326204f69a39d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Atte=20Kein=C3=A4nen?= <atte.keinanen@gmail.com>
Date: Mon, 14 Aug 2017 10:25:37 -0700
Subject: [PATCH] Fix issue with waitForAction, add documentation [ci e2e]

---
 frontend/test/__support__/integrated_tests.js | 201 ++++++++++--------
 .../admin/settings/settings.integ.spec.js     |   1 -
 2 files changed, 108 insertions(+), 94 deletions(-)

diff --git a/frontend/test/__support__/integrated_tests.js b/frontend/test/__support__/integrated_tests.js
index 712b26dd997..1ba351c7db2 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 7b7d8d1d88a..b7be2fba0b9 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])
         });
-- 
GitLab