From 40ead8050aa09333b3a4054c81db351d51c75bbc Mon Sep 17 00:00:00 2001
From: Sameer Al-Sakran <sameer@expa.com>
Date: Wed, 5 Jul 2017 18:25:14 -0700
Subject: [PATCH] basic positive path tests

---
 .../metabase/__support__/integrated_tests.js  | 274 +++++++++---
 .../__support__/integrated_tests_mocks.js     |  36 ++
 .../__support__/sample_dataset_fixture.js     |   1 -
 frontend/src/metabase/components/Tooltip.jsx  |  68 +++
 frontend/src/metabase/lib/api.js              |  12 +-
 frontend/src/metabase/questions/questions.js  |   2 +-
 frontend/src/metabase/redux/metadata.js       |  20 +-
 frontend/src/metabase/reference/reference.js  |   2 +-
 .../src/metabase/reference/reference.spec.js  | 333 ++++++++++----
 frontend/src/metabase/store.js                |  29 +-
 .../integration/reference/reference.spec.js   | 411 +++++++++++++++++-
 frontend/test/unit/reference/utils.spec.js    | 194 +--------
 package.json                                  |  11 +-
 13 files changed, 1020 insertions(+), 373 deletions(-)
 create mode 100644 frontend/src/metabase/__support__/integrated_tests_mocks.js

diff --git a/frontend/src/metabase/__support__/integrated_tests.js b/frontend/src/metabase/__support__/integrated_tests.js
index 1202f477c4c..a8ff718b40c 100644
--- a/frontend/src/metabase/__support__/integrated_tests.js
+++ b/frontend/src/metabase/__support__/integrated_tests.js
@@ -1,31 +1,72 @@
-import { BackendResource } from "../../../test/e2e/support/backend.js";
+/* global process, jasmine */
+
+/**
+ * Import this file before other imports in integrated tests
+ */
+
+// Mocks in a separate file as they would clutter this file
+// This must be before all other imports
+import "./integrated_tests_mocks";
+
+import { format as urlFormat } from "url";
 import api from "metabase/lib/api";
-import { SessionApi } from "metabase/services";
+import { CardApi, SessionApi } from "metabase/services";
 import { METABASE_SESSION_COOKIE } from "metabase/lib/cookies";
+import reducers from 'metabase/reducers-main';
 
 import React from 'react'
-import { Provider } from 'react-redux'
-import reducers from 'metabase/reducers-main';
+import { Provider } from 'react-redux';
 
-import MetabaseSettings from "metabase/lib/settings";
+import { createMemoryHistory } from 'history'
+import { getStore } from "metabase/store";
+import { createRoutes, Router, useRouterHistory } from "react-router";
+import _ from 'underscore';
 
-import { getStore } from 'metabase/store'
+// 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 } from "metabase/routes";
 
-// Stores the current login session
-var loginSession = null;
+let hasCreatedStore = false;
+let loginSession = null; // Stores the current login session
+let simulateOfflineMode = false;
 
 /**
  * Login to the Metabase test instance with default credentials
  */
 export async function login() {
+    if (hasCreatedStore) {
+        console.warn(
+            "Warning: You have created a test store before calling login() which means that up-to-date site settings " +
+            "won't be in the store unless you call `refreshSiteSettings` action manually. Please prefer " +
+            "logging in before all tests and creating the store inside an individual test or describe block."
+        )
+    }
+
     loginSession = await SessionApi.create({ email: "bob@metabase.com", password: "12341234"});
 }
 
+/**
+ * Calls the provided function while simulating that the browser is offline.
+ */
+export async function whenOffline(callWhenOffline) {
+    simulateOfflineMode = true;
+    return callWhenOffline()
+        .then((result) => {
+            simulateOfflineMode = false;
+            return result;
+        })
+        .catch((e) => {
+            simulateOfflineMode = false;
+            throw e;
+        });
+}
+
+
 // 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, body, data, options) => {
+api._makeRequest = async (method, url, headers, requestBody, data, options) => {
     const headersWithSessionCookie = {
         ...headers,
         ...(loginSession ? {"Cookie": `${METABASE_SESSION_COOKIE}=${loginSession.id}`} : {})
@@ -35,64 +76,191 @@ api._makeRequest = async (method, url, headers, body, data, options) => {
         credentials: "include",
         method,
         headers: new Headers(headersWithSessionCookie),
-        ...(body ? {body} : {})
+        ...(requestBody ? { body: requestBody } : {})
     };
 
-    const result = await fetch(api.basename + url, fetchOptions);
+    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) {
-        return result.json();
+        return resultBody
     } else {
-        throw { status: result.status, data: result.json() }
+        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.dir(error, { depth: null });
+            console.log(`The original request: ${method} ${url}`);
+            if (requestBody) console.log(`Original payload: ${requestBody}`);
+        }
+        throw error
     }
 }
 
-// disable GA for integrated tests (needed for UI tests)
-MetabaseSettings.on("anon_tracking_enabled", () => {
-    window['ga-disable-' + MetabaseSettings.get('ga_code')] = true;
-});
-
-// Reference to the reusable/shared backend server resource
-const server = BackendResource.get({});
 // Set the correct base url to metabase/lib/api module
-api.basename = server.host;
+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)
+}
 
 /**
- * Starts the backend process. Promise resolves when the backend has properly been initialized.
- * If the backend is already running, this resolves immediately
- * TODO: Should happen automatically before any tests have been run
+ * 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
+ *     * waiting until specific Redux actions have been dispatched
+ *     * getting a React container subtree for the current route
  */
-export const startServer = async () => {
-    await BackendResource.start(server);
+
+export const createTestStore = () => {
+    hasCreatedStore = true;
+
+    const history = useRouterHistory(createMemoryHistory)();
+    const store = getStore(reducers, history, undefined, (createStore) => testStoreEnhancer(createStore, history));
+    store.setFinalStoreInstance(store);
+
+    store.dispatch(refreshSiteSettings());
+    return store;
 }
 
-/**
- * Stops the current backend process
- * TODO: This should happen automatically after tests have been run
- */
-export const stopServer = async () => await BackendResource.stop(server);
+const testStoreEnhancer = (createStore, history) => {
+    return (...args) => {
+        const store = createStore(...args);
 
-/**
- * A Redux store that is shared between subsequent tests,
- * intended to reduce the need for reloading metadata between every test
- *
- * The Redux store matches the production app's store 1-to-1 with an exception that
- * it doesn't contain redux-router navigation history state
- */
-export const globalReduxStore = getStore(reducers);
-// needed in some tests after server startup:
-// globalReduxStore.dispatch(refreshSiteSettings());
+        const testStoreExtensions = {
+            _originalDispatch: store.dispatch,
+            _onActionDispatched: null,
+            _dispatchedActions: [],
+            _finalStoreInstance: null,
 
-/**
- * Returns the given React container with an access to a global Redux store
- */
-export function linkContainerToGlobalReduxStore(component) {
-    return (
-        <Provider store={globalReduxStore}>
-            {component}
-        </Provider>
-    );
+            setFinalStoreInstance: (finalStore) => {
+                store._finalStoreInstance = finalStore;
+            },
+
+            dispatch: (action) => {
+                const result = store._originalDispatch(action);
+                store._dispatchedActions = store._dispatchedActions.concat([action]);
+                if (store._onActionDispatched) store._onActionDispatched();
+                return result;
+            },
+
+            resetDispatchedActions: () => {
+                store._dispatchedActions = [];
+            },
+
+            /**
+             * Waits until all actions with given type identifiers have been called or fails if the maximum waiting
+             * time defined in `timeout` is exceeded.
+             *
+             * Convenient in tests for waiting specific actions to be executed after mounting a React container.
+             */
+            waitForActions: (actionTypes, {timeout = 2000} = {}) => {
+                actionTypes = Array.isArray(actionTypes) ? actionTypes : [actionTypes]
+
+                const allActionsAreTriggered = () => _.every(actionTypes, actionType =>
+                    store._dispatchedActions.filter((action) => action.type === actionType).length > 0
+                );
+
+                if (allActionsAreTriggered()) {
+                    // Short-circuit if all action types are already in the history of dispatched actions
+                    return;
+                } else {
+                    return new Promise((resolve, reject) => {
+                        store._onActionDispatched = () => {
+                            if (allActionsAreTriggered()) resolve()
+                        };
+                        setTimeout(() => {
+                            store._onActionDispatched = null;
+
+                            if (allActionsAreTriggered()) {
+                                // TODO: Figure out why we sometimes end up here instead of _onActionDispatched hook
+                                resolve()
+                            } 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(", ")}`
+                                    )
+                                )
+                            }
+
+                        }, timeout)
+                    });
+                }
+            },
+
+            getDispatchedActions: () => {
+                return store._dispatchedActions;
+            },
+
+            pushPath: (path) => history.push(path),
+            getPath: () => urlFormat(history.getCurrentLocation()),
+
+            connectContainer: (reactContainer) => {
+                // exploratory approach, not sure if this can ever work:
+                // return store._connectWithStore(reactContainer)
+                const routes = createRoutes(getRoutes(store._finalStoreInstance))
+                return store._connectWithStore(
+                    <Router
+                        routes={routes}
+                        history={history}
+                        render={(props) => React.cloneElement(reactContainer, props)}
+                    />
+                );
+            },
+
+            getAppContainer: () => {
+                return store._connectWithStore(
+                    <Router history={history}>
+                        {getRoutes(store._finalStoreInstance)}
+                    </Router>
+                )
+            },
+
+            _connectWithStore: (reactContainer) =>
+                <Provider store={store._finalStoreInstance}>
+                    {reactContainer}
+                </Provider>
+
+        }
+
+        return Object.assign(store, testStoreExtensions);
+    }
+}
+
+export const clickRouterLink = (linkEnzymeWrapper) =>
+    linkEnzymeWrapper.simulate('click', { button: 0 });
+
+// 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) => {
+    const savedCard = await CardApi.create(unsavedQuestion.card())
+    const savedQuestion = unsavedQuestion.setCard(savedCard);
+    savedQuestion._card = { ...savedQuestion._card, original_card_id: savedQuestion.id() }
+    return savedQuestion
 }
 
-// TODO: How to have the high timeout interval only for integration tests?
-// or even better, just for the setup/teardown of server process?
-jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000;
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
diff --git a/frontend/src/metabase/__support__/integrated_tests_mocks.js b/frontend/src/metabase/__support__/integrated_tests_mocks.js
new file mode 100644
index 00000000000..f66d6cfe0be
--- /dev/null
+++ b/frontend/src/metabase/__support__/integrated_tests_mocks.js
@@ -0,0 +1,36 @@
+import React from 'react'
+
+global.ga = () => {}
+global.ace.define = () => {}
+global.ace.require = () => {}
+
+global.window.matchMedia = () => ({ addListener: () => {}, removeListener: () => {} })
+
+jest.mock('metabase/lib/analytics');
+
+jest.mock("ace/ace", () => {}, {virtual: true});
+jest.mock("ace/mode-plain_text", () => {}, {virtual: true});
+jest.mock("ace/mode-javascript", () => {}, {virtual: true});
+jest.mock("ace/mode-json", () => {}, {virtual: true});
+jest.mock("ace/mode-clojure", () => {}, {virtual: true});
+jest.mock("ace/mode-ruby", () => {}, {virtual: true});
+jest.mock("ace/mode-html", () => {}, {virtual: true});
+jest.mock("ace/mode-jsx", () => {}, {virtual: true});
+jest.mock("ace/mode-sql", () => {}, {virtual: true});
+jest.mock("ace/mode-mysql", () => {}, {virtual: true});
+jest.mock("ace/mode-pgsql", () => {}, {virtual: true});
+jest.mock("ace/mode-sqlserver", () => {}, {virtual: true});
+jest.mock("ace/snippets/sql", () => {}, {virtual: true});
+jest.mock("ace/snippets/mysql", () => {}, {virtual: true});
+jest.mock("ace/snippets/pgsql", () => {}, {virtual: true});
+jest.mock("ace/snippets/sqlserver", () => {}, {virtual: true});
+jest.mock("ace/snippets/json", () => {}, {virtual: true});
+jest.mock("ace/snippets/json", () => {}, {virtual: true});
+jest.mock("ace/ext-language_tools", () => {}, {virtual: true});
+
+import * as modal from "metabase/components/Modal";
+const MockedModal = () => <div className="mocked-modal" />;
+modal.default = MockedModal;
+
+import * as tooltip from "metabase/components/Tooltip";
+tooltip.default = tooltip.TestTooltip
\ No newline at end of file
diff --git a/frontend/src/metabase/__support__/sample_dataset_fixture.js b/frontend/src/metabase/__support__/sample_dataset_fixture.js
index f043fa256dd..801c717fe04 100644
--- a/frontend/src/metabase/__support__/sample_dataset_fixture.js
+++ b/frontend/src/metabase/__support__/sample_dataset_fixture.js
@@ -1,5 +1,4 @@
 import { getMetadata } from "metabase/selectors/metadata";
-import { assocIn } from "icepick";
 
 export const DATABASE_ID = 1;
 export const ANOTHER_DATABASE_ID = 2;
diff --git a/frontend/src/metabase/components/Tooltip.jsx b/frontend/src/metabase/components/Tooltip.jsx
index 7b8d6f5fbb4..e1fd7961179 100644
--- a/frontend/src/metabase/components/Tooltip.jsx
+++ b/frontend/src/metabase/components/Tooltip.jsx
@@ -90,3 +90,71 @@ export default class Tooltip extends Component {
         return React.Children.only(this.props.children);
     }
 }
+
+/**
+ * Modified version of Tooltip for Jest/Enzyme tests. Instead of manipulating the document root it
+ * renders the tooltip content (in TestTooltipContent) next to "children" / hover area (TestTooltipHoverArea).
+ *
+ * The test tooltip can only be toggled with `jestWrapper.simulate("mouseenter")` and `jestWrapper.simulate("mouseleave")`.
+ */
+export class TestTooltip extends Component {
+    constructor(props, context) {
+        super(props, context);
+
+        this.state = {
+            isOpen: false,
+            isHovered: false
+        };
+    }
+
+    static propTypes = {
+        tooltip: PropTypes.node,
+        children: PropTypes.element.isRequired,
+        isEnabled: PropTypes.bool,
+        verticalAttachments: PropTypes.array,
+        isOpen: PropTypes.bool
+    };
+
+    static defaultProps = {
+        isEnabled: true,
+        verticalAttachments: ["top", "bottom"]
+    };
+
+    _onMouseEnter = (e) => {
+        this.setState({ isOpen: true, isHovered: true });
+    }
+
+    _onMouseLeave = (e) => {
+        this.setState({ isOpen: false, isHovered: false });
+    }
+
+    render() {
+        const { isEnabled, tooltip } = this.props;
+        const isOpen = this.props.isOpen != null ? this.props.isOpen : this.state.isOpen;
+
+        return (
+            <div>
+                <TestTooltipTarget
+                    onMouseEnter={this._onMouseEnter}
+                    onMouseLeave={this._onMouseLeave}
+                >
+                    {this.props.children}
+                </TestTooltipTarget>
+
+                { tooltip && isEnabled && isOpen &&
+                <TestTooltipContent>
+                    <TooltipPopover isOpen={true} target={this} {...this.props} children={this.props.tooltip} />
+                    {this.props.tooltip}
+                </TestTooltipContent>
+                }
+            </div>
+        )
+    }
+}
+
+export const TestTooltipTarget = ({ children, onMouseEnter, onMouseLeave }) =>
+    <div className="test-tooltip-hover-area" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
+        {children}
+    </div>
+
+export const TestTooltipContent = ({ children }) => <div className="test-tooltip-content">{children}</div>
\ No newline at end of file
diff --git a/frontend/src/metabase/lib/api.js b/frontend/src/metabase/lib/api.js
index 6062ea64975..ea661c351c4 100644
--- a/frontend/src/metabase/lib/api.js
+++ b/frontend/src/metabase/lib/api.js
@@ -1,4 +1,4 @@
-/* @flow */
+/* @flow weak */
 
 import querystring from "querystring";
 
@@ -82,8 +82,10 @@ class Api extends EventEmitter {
         }
     }
 
+    // TODO Atte Keinänen 6/26/17: Replacing this with isomorphic-fetch could simplify the implementation
     _makeRequest(method, url, headers, body, data, options) {
         return new Promise((resolve, reject) => {
+            let isCancelled = false;
             let xhr = new XMLHttpRequest();
             xhr.open(method, this.basename + url);
             for (let headerName in headers) {
@@ -102,7 +104,8 @@ class Api extends EventEmitter {
                     } else {
                         reject({
                             status: xhr.status,
-                            data: body
+                            data: body,
+                            isCancelled: isCancelled
                         });
                     }
                     if (!options.noEvent) {
@@ -113,7 +116,10 @@ class Api extends EventEmitter {
             xhr.send(body);
 
             if (options.cancelled) {
-                options.cancelled.then(() => xhr.abort());
+                options.cancelled.then(() => {
+                    isCancelled = true;
+                    xhr.abort()
+                });
             }
         });
     }
diff --git a/frontend/src/metabase/questions/questions.js b/frontend/src/metabase/questions/questions.js
index 02d4924030d..b09640bddec 100644
--- a/frontend/src/metabase/questions/questions.js
+++ b/frontend/src/metabase/questions/questions.js
@@ -26,7 +26,7 @@ const card = new schema.Entity('cards', {
 
 import { CardApi, CollectionsApi } from "metabase/services";
 
-const LOAD_ENTITIES = 'metabase/questions/LOAD_ENTITIES';
+export const LOAD_ENTITIES = 'metabase/questions/LOAD_ENTITIES';
 const SET_SEARCH_TEXT = 'metabase/questions/SET_SEARCH_TEXT';
 const SET_ITEM_SELECTED = 'metabase/questions/SET_ITEM_SELECTED';
 const SET_ALL_SELECTED = 'metabase/questions/SET_ALL_SELECTED';
diff --git a/frontend/src/metabase/redux/metadata.js b/frontend/src/metabase/redux/metadata.js
index 0e7de82de78..24654bbf3da 100644
--- a/frontend/src/metabase/redux/metadata.js
+++ b/frontend/src/metabase/redux/metadata.js
@@ -17,7 +17,7 @@ import _ from "underscore";
 
 import { MetabaseApi, MetricApi, SegmentApi, RevisionsApi } from "metabase/services";
 
-const FETCH_METRICS = "metabase/metadata/FETCH_METRICS";
+export const FETCH_METRICS = "metabase/metadata/FETCH_METRICS";
 export const fetchMetrics = createThunkAction(FETCH_METRICS, (reload = false) => {
     return async (dispatch, getState) => {
         const requestStatePath = ["metadata", "metrics"];
@@ -82,7 +82,7 @@ export const updateMetricImportantFields = createThunkAction(UPDATE_METRIC_IMPOR
 });
 
 
-const FETCH_SEGMENTS = "metabase/metadata/FETCH_SEGMENTS";
+export const FETCH_SEGMENTS = "metabase/metadata/FETCH_SEGMENTS";
 export const fetchSegments = createThunkAction(FETCH_SEGMENTS, (reload = false) => {
     return async (dispatch, getState) => {
         const requestStatePath = ["metadata", "segments"];
@@ -125,7 +125,7 @@ export const updateSegment = createThunkAction(UPDATE_SEGMENT, function(segment)
     };
 });
 
-const FETCH_DATABASES = "metabase/metadata/FETCH_DATABASES";
+export const FETCH_DATABASES = "metabase/metadata/FETCH_DATABASES";
 export const fetchDatabases = createThunkAction(FETCH_DATABASES, (reload = false) => {
     return async (dispatch, getState) => {
         const requestStatePath = ["metadata", "databases"];
@@ -146,7 +146,7 @@ export const fetchDatabases = createThunkAction(FETCH_DATABASES, (reload = false
     };
 });
 
-const FETCH_DATABASE_METADATA = "metabase/metadata/FETCH_DATABASE_METADATA";
+export const FETCH_DATABASE_METADATA = "metabase/metadata/FETCH_DATABASE_METADATA";
 export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(dbId, reload = false) {
     return async function(dispatch, getState) {
         const requestStatePath = ["metadata", "databases", dbId];
@@ -302,7 +302,7 @@ export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
     };
 });
 
-const FETCH_REVISIONS = "metabase/metadata/FETCH_REVISIONS";
+export const FETCH_REVISIONS = "metabase/metadata/FETCH_REVISIONS";
 export const fetchRevisions = createThunkAction(FETCH_REVISIONS, (type, id, reload = false) => {
     return async (dispatch, getState) => {
         const requestStatePath = ["metadata", "revisions", type, id];
@@ -327,7 +327,7 @@ export const fetchRevisions = createThunkAction(FETCH_REVISIONS, (type, id, relo
 });
 
 // for fetches with data dependencies in /reference
-const FETCH_METRIC_TABLE = "metabase/metadata/FETCH_METRIC_TABLE";
+export const FETCH_METRIC_TABLE = "metabase/metadata/FETCH_METRIC_TABLE";
 export const fetchMetricTable = createThunkAction(FETCH_METRIC_TABLE, (metricId, reload = false) => {
     return async (dispatch, getState) => {
         await dispatch(fetchMetrics()); // FIXME: fetchMetric?
@@ -337,7 +337,7 @@ export const fetchMetricTable = createThunkAction(FETCH_METRIC_TABLE, (metricId,
     };
 });
 
-const FETCH_METRIC_REVISIONS = "metabase/metadata/FETCH_METRIC_REVISIONS";
+export const FETCH_METRIC_REVISIONS = "metabase/metadata/FETCH_METRIC_REVISIONS";
 export const fetchMetricRevisions = createThunkAction(FETCH_METRIC_REVISIONS, (metricId, reload = false) => {
     return async (dispatch, getState) => {
         await Promise.all([
@@ -350,7 +350,7 @@ export const fetchMetricRevisions = createThunkAction(FETCH_METRIC_REVISIONS, (m
     };
 });
 
-const FETCH_SEGMENT_FIELDS = "metabase/metadata/FETCH_SEGMENT_FIELDS";
+export const FETCH_SEGMENT_FIELDS = "metabase/metadata/FETCH_SEGMENT_FIELDS";
 export const fetchSegmentFields = createThunkAction(FETCH_SEGMENT_FIELDS, (segmentId, reload = false) => {
     return async (dispatch, getState) => {
         await dispatch(fetchSegments()); // FIXME: fetchSegment?
@@ -363,7 +363,7 @@ export const fetchSegmentFields = createThunkAction(FETCH_SEGMENT_FIELDS, (segme
     };
 });
 
-const FETCH_SEGMENT_TABLE = "metabase/metadata/FETCH_SEGMENT_TABLE";
+export const FETCH_SEGMENT_TABLE = "metabase/metadata/FETCH_SEGMENT_TABLE";
 export const fetchSegmentTable = createThunkAction(FETCH_SEGMENT_TABLE, (segmentId, reload = false) => {
     return async (dispatch, getState) => {
         await dispatch(fetchSegments()); // FIXME: fetchSegment?
@@ -373,7 +373,7 @@ export const fetchSegmentTable = createThunkAction(FETCH_SEGMENT_TABLE, (segment
     };
 });
 
-const FETCH_SEGMENT_REVISIONS = "metabase/metadata/FETCH_SEGMENT_REVISIONS";
+export const FETCH_SEGMENT_REVISIONS = "metabase/metadata/FETCH_SEGMENT_REVISIONS";
 export const fetchSegmentRevisions = createThunkAction(FETCH_SEGMENT_REVISIONS, (segmentId, reload = false) => {
     return async (dispatch, getState) => {
         await Promise.all([
diff --git a/frontend/src/metabase/reference/reference.js b/frontend/src/metabase/reference/reference.js
index 4934bed947d..94d42e0cbff 100644
--- a/frontend/src/metabase/reference/reference.js
+++ b/frontend/src/metabase/reference/reference.js
@@ -17,7 +17,7 @@ import {
     isEmptyObject 
 } from "./utils.js"
 
-const FETCH_GUIDE = "metabase/reference/FETCH_GUIDE";
+export const FETCH_GUIDE = "metabase/reference/FETCH_GUIDE";
 const SET_ERROR = "metabase/reference/SET_ERROR";
 const CLEAR_ERROR = "metabase/reference/CLEAR_ERROR";
 const START_LOADING = "metabase/reference/START_LOADING";
diff --git a/frontend/src/metabase/reference/reference.spec.js b/frontend/src/metabase/reference/reference.spec.js
index 430cc4b26eb..66f20d21627 100644
--- a/frontend/src/metabase/reference/reference.spec.js
+++ b/frontend/src/metabase/reference/reference.spec.js
@@ -1,34 +1,51 @@
 /* @flow weak */
 
-// import { DATABASE_ID, ORDERS_TABLE_ID, metadata } from "metabase/__support__/sample_dataset_fixture";
 import {
-    linkContainerToGlobalReduxStore,
-    globalReduxStore as store,
     login,
-    startServer,
-    stopServer
+    createTestStore
 } from "metabase/__support__/integrated_tests";
 
 import React from 'react';
 import { shallow, mount, render } from 'enzyme';
 
 import { CardApi, SegmentApi, MetricApi } from 'metabase/services'
-import ReferenceEntity from "metabase/reference/containers/ReferenceEntity";
 import { fetchMetrics } from "metabase/redux/metadata";
-
-/**
- * Returns a reference section container linked to the global test Redux store
- *
- * @param Container: the container element like ReferenceEntity, ReferenceEntityList or ReferenceFieldsList
- * @param sectionId: either "metrics", "segments" or "databases"
- * @param params: a key-value list of section-specific route parameters, e.g. { segmentId: "1" }
- */
-const getReferenceContainer = (Container, sectionId, params = {}) => {
-    return linkContainerToGlobalReduxStore(<Container
-        location={{ pathname: {sectionId} }}
-        params={params}
-    />)
-}
+import { 
+    FETCH_DATABASE_METADATA,
+    FETCH_DATABASES,
+    FETCH_METRICS,
+    FETCH_SEGMENTS,
+    FETCH_SEGMENT_TABLE,
+    FETCH_SEGMENT_FIELDS,
+    FETCH_METRIC_TABLE,
+    FETCH_SEGMENT_REVISIONS,
+    FETCH_METRIC_REVISIONS
+} from "metabase/redux/metadata";
+
+import { FETCH_GUIDE } from "metabase/reference/reference"
+import { LOAD_ENTITIES } from "metabase/questions/questions"
+
+import DatabaseListContainer from "metabase/reference/databases/DatabaseListContainer";
+import DatabaseDetailContainer from "metabase/reference/databases/DatabaseDetailContainer";
+import TableListContainer from "metabase/reference/databases/TableListContainer";
+import TableDetailContainer from "metabase/reference/databases/TableDetailContainer";
+import TableQuestionsContainer from "metabase/reference/databases/TableQuestionsContainer";
+import FieldListContainer from "metabase/reference/databases/FieldListContainer";
+import FieldDetailContainer from "metabase/reference/databases/FieldDetailContainer";
+
+import GettingStartedGuideContainer from "metabase/reference/guide/GettingStartedGuideContainer";
+
+import SegmentListContainer from "metabase/reference/segments/SegmentListContainer";
+import SegmentDetailContainer from "metabase/reference/segments/SegmentDetailContainer";
+import SegmentQuestionsContainer from "metabase/reference/segments/SegmentQuestionsContainer";
+import SegmentRevisionsContainer from "metabase/reference/segments/SegmentRevisionsContainer";
+import SegmentFieldListContainer from "metabase/reference/segments/SegmentFieldListContainer";
+import SegmentFieldDetailContainer from "metabase/reference/segments/SegmentFieldDetailContainer";
+
+import MetricListContainer from "metabase/reference/metrics/MetricListContainer";
+import MetricDetailContainer from "metabase/reference/metrics/MetricDetailContainer";
+import MetricQuestionsContainer from "metabase/reference/metrics/MetricQuestionsContainer";
+import MetricRevisionsContainer from "metabase/reference/metrics/MetricRevisionsContainer";
 
 
 describe("The Reference Section", () => {
@@ -57,40 +74,33 @@ describe("The Reference Section", () => {
 
     // Scaffolding
     beforeAll(async () => {
-        await startServer();
         await login();
 
     })
 
-    afterAll(async () => {
-        await stopServer();
-    })
-
 
-   it("do stuff", async () => {
-
-        // browserHistory.replace("/");
-
-        expect(true).toBe(true);
-    })
-
-
-
-    describe("The Getting Started Guide", ()=>{
-
-        it("Should show an empty guide for non-admin users", async () => {expect(true).toBe(true)})
+    describe("The Getting Started Guide", async ()=>{
+        
         
-        it("Should show an empty guide with a creation CTA for admin users", async () => {expect(true).toBe(true)})
+        it("Should show an empty guide for non-admin users", async () => {
+            const store = await createTestStore()    
+            store.pushPath("/reference/");
+            const container = mount(store.connectContainer(<GettingStartedGuideContainer />));
+            await store.waitForActions([FETCH_DATABASE_METADATA, FETCH_SEGMENTS, FETCH_METRICS])
+        })
+        
+        it("Should show an empty guide with a creation CTA for admin users", async () => {
+            // TODO
+            expect(true).toBe(true)
+        })
 
-        it("A non-admin attempting to edit the guide should get an error", async () => {expect(true).toBe(true)})
+        it("A non-admin attempting to edit the guide should get an error", async () => {
+            // TODO
+            expect(true).toBe(true)
+        })
 
         it("Adding metrics should to the guide should make them appear", async () => {
-            // load needed metadata already (as you can check from the `referenceSections` object in reference/selectors.js)
-            // normally ReferenceApp container does the loading by calling `tryFetchData`
-            await store.dispatch(fetchMetrics())
-            const mountedContainer = mount(getReferenceContainer(ReferenceEntity, "metrics"));
-            console.log('the dom tree that enzyme has rendered', mountedContainer.debug())
-
+            
             expect(0).toBe(0)
             var metric = await MetricApi.create(metricDef);
             expect(1).toBe(1)
@@ -118,42 +128,78 @@ describe("The Reference Section", () => {
     
     describe("The Metrics section of the Data Reference", async ()=>{
         describe("Empty State", async () => {
-            // metrics list
-            // metric detail
-            // metrics questions 
-            // metrics revisions
+
+            it("Should show no metrics in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics");
+                const container = mount(store.connectContainer(<MetricListContainer />));
+                await store.waitForActions([FETCH_METRICS])
+            })
 
         });
 
         describe("With Metrics State", async () => {
-         var metricIds = []
-        var segmentIds = []
+            var metricIds = []
+            var segmentIds = []
 
-        beforeAll(async () => {            
-            // Create some metrics to have something to look at
-            var metric = await MetricApi.create(metricDef);
-            var metric2 = await MetricApi.create(anotherMetricDef);
-            
-            metricIds.push(metric.id)
-            metricIds.push(metric2.id)
+            beforeAll(async () => {            
+                // Create some metrics to have something to look at
+                var metric = await MetricApi.create(metricDef);
+                var metric2 = await MetricApi.create(anotherMetricDef);
+                
+                metricIds.push(metric.id)
+                metricIds.push(metric2.id)
+                console.log(metricIds)
+                })
 
+            afterAll(async () => {
+                // Delete the guide we created
+                // remove the metrics we created   
+                // This is a bit messy as technically these are just archived
+                for (const id of metricIds){
+                    await MetricApi.delete({metricId: id, revision_message: "Please"})
+                }
+            })
+            // metrics list
+            it("Should show no metrics in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics");
+                const container = mount(store.connectContainer(<MetricListContainer />));
+                await store.waitForActions([FETCH_METRICS])
+            })
+            // metric detail
+            it("Should show the metric detail view for a specific id", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics/"+metricIds[0]);
+                const container = mount(store.connectContainer(<MetricDetailContainer />));
+                await store.waitForActions([FETCH_METRIC_TABLE, FETCH_GUIDE])
+            })
+            // metrics questions 
+            it("Should show no questions based on a new metric", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics/"+metricIds[0]+'/questions');
+                const container = mount(store.connectContainer(<MetricQuestionsContainer />));
+                await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE])
+            })
+            // metrics revisions
+            it("Should show revisions", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics/"+metricIds[0]+'/revisions');
+                const container = mount(store.connectContainer(<MetricRevisionsContainer />));
+                await store.waitForActions([FETCH_METRICS, FETCH_METRIC_REVISIONS])
             })
 
-        afterAll(async () => {
-            // Delete the guide we created
-            // remove the metrics we created   
-            // This is a bit messy as technically these are just archived
-            for (const id of metricIds){
-                await MetricApi.delete({metricId: id, revision_message: "Please"})
-            }
-        })
-
-        it("Should see a newly asked question in its questions list", async () => {
-                var card = await CardApi.create(metricCardDef)
-
-                expect(card.name).toBe(metricCardDef.name);
-                
-                await CardApi.delete({cardId: card.id})
+            it("Should see a newly asked question in its questions list", async () => {
+                    var card = await CardApi.create(metricCardDef)
+
+                    expect(card.name).toBe(metricCardDef.name);
+                    // see that there is a new question on the metric's questions page
+                    const store = await createTestStore()    
+                    store.pushPath("/reference/metrics/"+metricIds[0]+'/questions');
+                    const container = mount(store.connectContainer(<MetricQuestionsContainer />));
+                    await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE])
+                    
+                    await CardApi.delete({cardId: card.id})
             })
 
                        
@@ -163,15 +209,16 @@ describe("The Reference Section", () => {
     describe("The Segments section of the Data Reference", async ()=>{
 
         describe("Empty State", async () => {
-                        
+                it("Should show no segments in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments");
+                const container = mount(store.connectContainer(<SegmentListContainer />));
+                await store.waitForActions([FETCH_SEGMENTS])
+            })
+
         });
-        // segments list
-        // segments detail
-        // segments field list
-        // segments field detail
-        // segments questions
-        // segments revisions
-        describe("With Segments State", async () => {
+
+        fdescribe("With Segments State", async () => {
             var segmentIds = []
 
             beforeAll(async () => {            
@@ -192,12 +239,65 @@ describe("The Reference Section", () => {
                 }
             })
 
+
+            // segments list
+            it("Should show the segments in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments");
+                const container = mount(store.connectContainer(<SegmentListContainer />));
+                await store.waitForActions([FETCH_SEGMENTS])
+            })
+            // segment detail
+            it("Should show the segment detail view for a specific id", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]);
+                const container = mount(store.connectContainer(<SegmentDetailContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE])
+            })
+
+            // segments field list
+            it("Should show the segment fields list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+"/fields");
+                const container = mount(store.connectContainer(<SegmentFieldListContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS])
+            })
+            // segment detail
+            it("Should show the segment field detail view for a specific id", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+"/fields/" + 1);
+                const container = mount(store.connectContainer(<SegmentFieldDetailContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS])
+            })
+
+            // segment questions 
+            it("Should show no questions based on a new segment", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+'/questions');
+                const container = mount(store.connectContainer(<SegmentQuestionsContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES])
+            })
+            // segment revisions
+            it("Should show revisions", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+'/revisions');
+                const container = mount(store.connectContainer(<SegmentRevisionsContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_REVISIONS])
+            })
+
+
+
             it("Should see a newly asked question in its questions list", async () => {
                 var card = await CardApi.create(segmentCardDef)
 
                 expect(card.name).toBe(segmentCardDef.name);
                 
                 await CardApi.delete({cardId: card.id})
+
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+'/questions');
+                const container = mount(store.connectContainer(<SegmentQuestionsContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES])
             })
                       
         });
@@ -206,36 +306,81 @@ describe("The Reference Section", () => {
     describe("The Data Reference for the Sample Database", async () => {
         
         // database list
-        it("should see a single database", async ()=>{})
+        it("should see a single database", async ()=>{
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/");
+            const container = mount(store.connectContainer(<DatabaseListContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASES"])
+        })
         
         // database detail
-        it("should see a the detail view for the sample database", async ()=>{})
+        it("should see a the detail view for the sample database", async ()=>{
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1");
+            const container = mount(store.connectContainer(<DatabaseDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+
+        })
         
         // table list
        it("should see the 4 tables in the sample database",async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables");
+            const container = mount(store.connectContainer(<TableListContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+
             expect(4).toBe(4);
         })
         // table detail
 
        it("should see the Orders table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
             expect(true).toBe(true);
         })
 
-       it("should see the People table", async  () => {
+       it("should see the Reviews table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/2");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
             expect(true).toBe(true);
         })
        it("should see the Products table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/3");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
             expect(true).toBe(true);
         })
-       it("should see the Reviews table", async  () => {
+       it("should see the People table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/4");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
             expect(true).toBe(true);
         })
         // field list
        it("should see the fields for the orders table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/fields");
+            const container = mount(store.connectContainer(<FieldListContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+
             expect(true).toBe(true);
         })
        it("should see the questions for the orders tables", async  () => {
 
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/questions");
+            const container = mount(store.connectContainer(<TableQuestionsContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+
+
             expect(true).toBe(true);
             
             var card = await CardApi.create(cardDef)
@@ -248,19 +393,19 @@ describe("The Reference Section", () => {
         // field detail
 
        it("should see the orders created_at timestamp field", async () => {
-            expect(true).toBe(true);
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/fields/1");
+            const container = mount(store.connectContainer(<FieldDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
         })
 
        it("should see the orders id field", async () => {
-            expect(true).toBe(true);
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/fields/25");
+            const container = mount(store.connectContainer(<FieldDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
         })
     });
 
 
-});
-
-
-    
-
-
-
+});
\ No newline at end of file
diff --git a/frontend/src/metabase/store.js b/frontend/src/metabase/store.js
index d80f41cc22f..92185f7bb6d 100644
--- a/frontend/src/metabase/store.js
+++ b/frontend/src/metabase/store.js
@@ -4,20 +4,36 @@ import { combineReducers, applyMiddleware, createStore, compose } from 'redux'
 import { reducer as form } from "redux-form";
 import { routerReducer as routing, routerMiddleware } from 'react-router-redux'
 
-import thunk from "redux-thunk";
 import promise from "redux-promise";
 import logger from "redux-logger";
 
 import { DEBUG } from "metabase/lib/debug";
 
-const devToolsExtension = window.devToolsExtension ? window.devToolsExtension() : (f => f);
+/**
+ * Provides the same functionality as redux-thunk and augments the dispatch method with
+ * `dispatch.action(type, payload)` which creates an action that adheres to Flux Standard Action format.
+ */
+const thunkWithDispatchAction = ({ dispatch, getState }) => next => action => {
+    if (typeof action === 'function') {
+        const dispatchAugmented = Object.assign(dispatch, {
+            action: (type, payload) => dispatch({ type, payload })
+        });
+
+        return action(dispatchAugmented, getState);
+    }
 
-let middleware = [thunk, promise];
+    return next(action);
+};
+
+let middleware = [thunkWithDispatchAction, promise];
 if (DEBUG) {
     middleware.push(logger);
 }
 
-export function getStore(reducers, history, intialState) {
+const devToolsExtension = window.devToolsExtension ? window.devToolsExtension() : (f => f);
+
+export function getStore(reducers, history, intialState, enhancer = (a) => a) {
+
     const reducer = combineReducers({
         ...reducers,
         form,
@@ -28,6 +44,7 @@ export function getStore(reducers, history, intialState) {
 
     return createStore(reducer, intialState, compose(
         applyMiddleware(...middleware),
-        devToolsExtension
+        devToolsExtension,
+        enhancer,
     ));
-}
+}
\ No newline at end of file
diff --git a/frontend/test/integration/reference/reference.spec.js b/frontend/test/integration/reference/reference.spec.js
index 05c30df63d9..cf5eaedcaa8 100644
--- a/frontend/test/integration/reference/reference.spec.js
+++ b/frontend/test/integration/reference/reference.spec.js
@@ -1,19 +1,412 @@
 /* @flow weak */
 
-import { DATABASE_ID, ORDERS_TABLE_ID, metadata } from "metabase/__support__/sample_dataset_fixture";
-import { login, startServer, stopServer } from "metabase/__support__/integrated_tests";
+// import { DATABASE_ID, ORDERS_TABLE_ID, metadata } from "metabase/__support__/sample_dataset_fixture";
+import {
+    login,
+    createTestStore
+} from "metabase/__support__/integrated_tests";
 
-describe("Testing testing", () => {
+import React from 'react';
+import { shallow, mount, render } from 'enzyme';
+
+import { CardApi, SegmentApi, MetricApi } from 'metabase/services'
+import { fetchMetrics } from "metabase/redux/metadata";
+import { 
+    FETCH_DATABASE_METADATA,
+    FETCH_DATABASES,
+    FETCH_METRICS,
+    FETCH_SEGMENTS,
+    FETCH_SEGMENT_TABLE,
+    FETCH_SEGMENT_FIELDS,
+    FETCH_METRIC_TABLE,
+    FETCH_SEGMENT_REVISIONS,
+    FETCH_METRIC_REVISIONS
+} from "metabase/redux/metadata";
+
+import { FETCH_GUIDE } from "metabase/reference/reference"
+import { LOAD_ENTITIES } from "metabase/questions/questions"
+
+import DatabaseListContainer from "metabase/reference/databases/DatabaseListContainer";
+import DatabaseDetailContainer from "metabase/reference/databases/DatabaseDetailContainer";
+import TableListContainer from "metabase/reference/databases/TableListContainer";
+import TableDetailContainer from "metabase/reference/databases/TableDetailContainer";
+import TableQuestionsContainer from "metabase/reference/databases/TableQuestionsContainer";
+import FieldListContainer from "metabase/reference/databases/FieldListContainer";
+import FieldDetailContainer from "metabase/reference/databases/FieldDetailContainer";
+
+import GettingStartedGuideContainer from "metabase/reference/guide/GettingStartedGuideContainer";
+
+import SegmentListContainer from "metabase/reference/segments/SegmentListContainer";
+import SegmentDetailContainer from "metabase/reference/segments/SegmentDetailContainer";
+import SegmentQuestionsContainer from "metabase/reference/segments/SegmentQuestionsContainer";
+import SegmentRevisionsContainer from "metabase/reference/segments/SegmentRevisionsContainer";
+import SegmentFieldListContainer from "metabase/reference/segments/SegmentFieldListContainer";
+import SegmentFieldDetailContainer from "metabase/reference/segments/SegmentFieldDetailContainer";
+
+import MetricListContainer from "metabase/reference/metrics/MetricListContainer";
+import MetricDetailContainer from "metabase/reference/metrics/MetricDetailContainer";
+import MetricQuestionsContainer from "metabase/reference/metrics/MetricQuestionsContainer";
+import MetricRevisionsContainer from "metabase/reference/metrics/MetricRevisionsContainer";
+
+
+describe("The Reference Section", () => {
+    // Test data
+    const segmentDef = {name: "A Segment", description: "I did it!", table_id: 1, show_in_getting_started: true,
+                        definition: {database: 1, query: {filter: ["abc"]}}}
+
+    const anotherSegmentDef = {name: "Another Segment", description: "I did it again!", table_id: 1, show_in_getting_started: true,
+                               definition:{database: 1, query: {filter: ["def"]}}}
+    const metricDef = {name: "A Metric", description: "I did it!", table_id: 1,show_in_getting_started: true,
+                        definition: {database: 1, query: {aggregation: ["count"]}}}
+
+    const anotherMetricDef = {name: "Another Metric", description: "I did it again!", table_id: 1,show_in_getting_started: true,
+                        definition: {database: 1, query: {aggregation: ["count"]}}}
+    
+    const cardDef = { name :"A card", display: "scalar", 
+                      dataset_query: {database: 1, table_id: 1, type: "query", query: {source_table: 1, "aggregation": ["count"]}},
+                      visualization_settings: {}}
+
+    const metricCardDef = { name :"A card", display: "scalar", 
+                      dataset_query: {database: 1, table_id: 1, type: "query", query: {source_table: 1, "aggregation": ["metric", 1]}},
+                      visualization_settings: {}}
+    const segmentCardDef = { name :"A card", display: "scalar", 
+                      dataset_query: {database: 1, table_id: 1, type: "query", query: {source_table: 1, "aggregation": ["count"], "filter": ["segment", 1]}},
+                      visualization_settings: {}}
+
+    // Scaffolding
     beforeAll(async () => {
-        await startServer();
         await login();
-    })
 
-   it("do stuff", async () => {
-        expect(true).toBe(true);
     })
 
-    afterAll(async () => {
-        await stopServer();
+
+    describe("The Getting Started Guide", async ()=>{
+        
+        
+        it("Should show an empty guide for non-admin users", async () => {
+            const store = await createTestStore()    
+            store.pushPath("/reference/");
+            const container = mount(store.connectContainer(<GettingStartedGuideContainer />));
+            await store.waitForActions([FETCH_DATABASE_METADATA, FETCH_SEGMENTS, FETCH_METRICS])
+        })
+        
+        it("Should show an empty guide with a creation CTA for admin users", async () => {
+            // TODO
+            expect(true).toBe(true)
+        })
+
+        it("A non-admin attempting to edit the guide should get an error", async () => {
+            // TODO
+            expect(true).toBe(true)
+        })
+
+        it("Adding metrics should to the guide should make them appear", async () => {
+            
+            expect(0).toBe(0)
+            var metric = await MetricApi.create(metricDef);
+            expect(1).toBe(1)
+            var metric2 = await MetricApi.create(anotherMetricDef);
+            expect(2).toBe(2)
+            await MetricApi.delete({metricId: metric.id, revision_message: "Please"})
+            expect(1).toBe(1)
+            await MetricApi.delete({metricId: metric2.id, revision_message: "Please"})
+            expect(0).toBe(0)
+        })
+
+        it("Adding segments should to the guide should make them appear", async () => {
+            expect(0).toBe(0)
+            var segment = await SegmentApi.create(segmentDef);
+            expect(1).toBe(1)
+            var anotherSegment = await SegmentApi.create(anotherSegmentDef);
+            expect(2).toBe(2)
+            await SegmentApi.delete({segmentId: segment.id, revision_message: "Please"})
+            expect(1).toBe(1)
+            await SegmentApi.delete({segmentId: anotherSegment.id, revision_message: "Please"})
+            expect(0).toBe(0)
+        })
+        
     })
+    
+    describe("The Metrics section of the Data Reference", async ()=>{
+        describe("Empty State", async () => {
+
+            it("Should show no metrics in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics");
+                const container = mount(store.connectContainer(<MetricListContainer />));
+                await store.waitForActions([FETCH_METRICS])
+            })
+
+        });
+
+        describe("With Metrics State", async () => {
+            var metricIds = []
+            var segmentIds = []
+
+            beforeAll(async () => {            
+                // Create some metrics to have something to look at
+                var metric = await MetricApi.create(metricDef);
+                var metric2 = await MetricApi.create(anotherMetricDef);
+                
+                metricIds.push(metric.id)
+                metricIds.push(metric2.id)
+                console.log(metricIds)
+                })
+
+            afterAll(async () => {
+                // Delete the guide we created
+                // remove the metrics we created   
+                // This is a bit messy as technically these are just archived
+                for (const id of metricIds){
+                    await MetricApi.delete({metricId: id, revision_message: "Please"})
+                }
+            })
+            // metrics list
+            it("Should show no metrics in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics");
+                const container = mount(store.connectContainer(<MetricListContainer />));
+                await store.waitForActions([FETCH_METRICS])
+            })
+            // metric detail
+            it("Should show the metric detail view for a specific id", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics/"+metricIds[0]);
+                const container = mount(store.connectContainer(<MetricDetailContainer />));
+                await store.waitForActions([FETCH_METRIC_TABLE, FETCH_GUIDE])
+            })
+            // metrics questions 
+            it("Should show no questions based on a new metric", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics/"+metricIds[0]+'/questions');
+                const container = mount(store.connectContainer(<MetricQuestionsContainer />));
+                await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE])
+            })
+            // metrics revisions
+            it("Should show revisions", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/metrics/"+metricIds[0]+'/revisions');
+                const container = mount(store.connectContainer(<MetricRevisionsContainer />));
+                await store.waitForActions([FETCH_METRICS, FETCH_METRIC_REVISIONS])
+            })
+
+            it("Should see a newly asked question in its questions list", async () => {
+                    var card = await CardApi.create(metricCardDef)
+
+                    expect(card.name).toBe(metricCardDef.name);
+                    // see that there is a new question on the metric's questions page
+                    const store = await createTestStore()    
+                    store.pushPath("/reference/metrics/"+metricIds[0]+'/questions');
+                    const container = mount(store.connectContainer(<MetricQuestionsContainer />));
+                    await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE])
+                    
+                    await CardApi.delete({cardId: card.id})
+            })
+
+                       
+        });
+    });
+    
+    describe("The Segments section of the Data Reference", async ()=>{
+
+        describe("Empty State", async () => {
+                it("Should show no segments in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments");
+                const container = mount(store.connectContainer(<SegmentListContainer />));
+                await store.waitForActions([FETCH_SEGMENTS])
+            })
+
+        });
+
+        fdescribe("With Segments State", async () => {
+            var segmentIds = []
+
+            beforeAll(async () => {            
+                // Create some segments to have something to look at
+                var segment = await SegmentApi.create(segmentDef);
+                var anotherSegment = await SegmentApi.create(anotherSegmentDef);
+                segmentIds.push(segment.id)
+                segmentIds.push(anotherSegment.id)
+
+                })
+
+            afterAll(async () => {
+                // Delete the guide we created
+                // remove the metrics  we created   
+                // This is a bit messy as technically these are just archived
+                for (const id of segmentIds){
+                    await SegmentApi.delete({segmentId: id, revision_message: "Please"})
+                }
+            })
+
+
+            // segments list
+            it("Should show the segments in the list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments");
+                const container = mount(store.connectContainer(<SegmentListContainer />));
+                await store.waitForActions([FETCH_SEGMENTS])
+            })
+            // segment detail
+            it("Should show the segment detail view for a specific id", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]);
+                const container = mount(store.connectContainer(<SegmentDetailContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE])
+            })
+
+            // segments field list
+            it("Should show the segment fields list", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+"/fields");
+                const container = mount(store.connectContainer(<SegmentFieldListContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS])
+            })
+            // segment detail
+            it("Should show the segment field detail view for a specific id", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+"/fields/" + 1);
+                const container = mount(store.connectContainer(<SegmentFieldDetailContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS])
+            })
+
+            // segment questions 
+            it("Should show no questions based on a new segment", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+'/questions');
+                const container = mount(store.connectContainer(<SegmentQuestionsContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES])
+            })
+            // segment revisions
+            it("Should show revisions", async () => {
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+'/revisions');
+                const container = mount(store.connectContainer(<SegmentRevisionsContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_REVISIONS])
+            })
+
+
+
+            it("Should see a newly asked question in its questions list", async () => {
+                var card = await CardApi.create(segmentCardDef)
+
+                expect(card.name).toBe(segmentCardDef.name);
+                
+                await CardApi.delete({cardId: card.id})
+
+                const store = await createTestStore()    
+                store.pushPath("/reference/segments/"+segmentIds[0]+'/questions');
+                const container = mount(store.connectContainer(<SegmentQuestionsContainer />));
+                await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES])
+            })
+                      
+        });
+    });
+    
+    describe("The Data Reference for the Sample Database", async () => {
+        
+        // database list
+        it("should see a single database", async ()=>{
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/");
+            const container = mount(store.connectContainer(<DatabaseListContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASES"])
+        })
+        
+        // database detail
+        it("should see a the detail view for the sample database", async ()=>{
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1");
+            const container = mount(store.connectContainer(<DatabaseDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+
+        })
+        
+        // table list
+       it("should see the 4 tables in the sample database",async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables");
+            const container = mount(store.connectContainer(<TableListContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+
+            expect(4).toBe(4);
+        })
+        // table detail
+
+       it("should see the Orders table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+        })
+
+       it("should see the Reviews table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/2");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+        })
+       it("should see the Products table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/3");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+        })
+       it("should see the People table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/4");
+            const container = mount(store.connectContainer(<TableDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+        })
+        // field list
+       it("should see the fields for the orders table", async  () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/fields");
+            const container = mount(store.connectContainer(<FieldListContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+
+            expect(true).toBe(true);
+        })
+       it("should see the questions for the orders tables", async  () => {
+
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/questions");
+            const container = mount(store.connectContainer(<TableQuestionsContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+            expect(true).toBe(true);
+
+
+            expect(true).toBe(true);
+            
+            var card = await CardApi.create(cardDef)
+
+            expect(card.name).toBe(cardDef.name);
+            
+            await CardApi.delete({cardId: card.id})
+        })
+
+        // field detail
+
+       it("should see the orders created_at timestamp field", async () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/fields/1");
+            const container = mount(store.connectContainer(<FieldDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+        })
+
+       it("should see the orders id field", async () => {
+            const store = await createTestStore()
+            store.pushPath("/reference/databases/1/tables/1/fields/25");
+            const container = mount(store.connectContainer(<FieldDetailContainer />));
+            await store.waitForActions(["metabase/metadata/FETCH_DATABASE_METADATA"])
+        })
+    });
+
+
 });
\ No newline at end of file
diff --git a/frontend/test/unit/reference/utils.spec.js b/frontend/test/unit/reference/utils.spec.js
index ed576007bcc..4da172ad2a7 100644
--- a/frontend/test/unit/reference/utils.spec.js
+++ b/frontend/test/unit/reference/utils.spec.js
@@ -1,148 +1,13 @@
 import {
-    tryFetchData,
-    tryUpdateData,
-    tryUpdateFields,
-    buildBreadcrumbs,
     databaseToForeignKeys,
-    separateTablesBySchema,
     getQuestion
 } from 'metabase/reference/utils';
 
+import { separateTablesBySchema } from "metabase/reference/databases/TableList"
 import { TYPE } from "metabase/lib/types";
 
 describe("Reference utils.js", () => {
-    const getProps = ({
-        section = {
-            fetch: {test1: [], test2: [2], test3: [3,4]}
-        },
-        entity = { foo: 'foo', bar: 'bar' },
-        entities = { foo: {foo: 'foo', bar: 'bar'}, bar: {foo: 'bar', bar: 'foo'} },
-        test1 = jasmine.createSpy('test1'),
-        test2 = jasmine.createSpy('test2'),
-        test3 = jasmine.createSpy('test3'),
-        updateField = jasmine.createSpy('updateField'),
-        clearError = jasmine.createSpy('clearError'),
-        resetForm = jasmine.createSpy('resetForm'),
-        endEditing = jasmine.createSpy('endEditing'),
-        startLoading = jasmine.createSpy('startLoading'),
-        setError = jasmine.createSpy('setError'),
-        endLoading = jasmine.createSpy('endLoading')
-    } = {}) => ({
-        section,
-        entity,
-        entities,
-        test1,
-        test2,
-        test3,
-        updateField,
-        clearError,
-        resetForm,
-        endEditing,
-        startLoading,
-        setError,
-        endLoading
-    });
-
-    describe("tryFetchData()", () => {
-        it("should call all fetch functions in section with correct arguments", async (done) => {
-            const props = getProps();
-            await tryFetchData(props);
-
-            expect(props.test1).toHaveBeenCalledWith();
-            expect(props.test2).toHaveBeenCalledWith(2);
-            expect(props.test3).toHaveBeenCalledWith(3, 4);
-            expect(props.clearError.calls.count()).toEqual(1);
-            expect(props.startLoading.calls.count()).toEqual(1);
-            expect(props.setError.calls.count()).toEqual(0);
-            expect(props.endLoading.calls.count()).toEqual(1);
-            done();
-        });
-
-        xit("should set error when error occurs", async () => {
-            const props = getProps(() => Promise.reject('test'));
-            tryFetchData(props).catch(error => console.error(error))
-
-            expect(props.test1).toHaveBeenCalledWith();
-            expect(props.test2).toHaveBeenCalledWith(2);
-            expect(props.test3).toHaveBeenCalledWith(3, 4);
-            expect(props.clearError.calls.count()).toEqual(1);
-            expect(props.startLoading.calls.count()).toEqual(1);
-            expect(props.setError.calls.count()).toEqual(0);
-            expect(props.endLoading.calls.count()).toEqual(1);
-        });
-    });
-
-    describe("tryUpdateData()", () => {
-        it("should call update function with merged entity", async (done) => {
-            const props = getProps({
-                section: {
-                    update: 'test1'
-                },
-                entity: { foo: 'foo', bar: 'bar' }
-            });
-            const fields = {bar: 'bar2'};
-
-            await tryUpdateData(fields, props);
-
-            expect(props.test1.calls.argsFor(0)[0]).toEqual({foo: 'foo', bar: 'bar2'});
-            expect(props.endEditing.calls.count()).toEqual(1);
-            expect(props.resetForm.calls.count()).toEqual(1);
-            expect(props.startLoading.calls.count()).toEqual(1);
-            expect(props.setError.calls.count()).toEqual(0);
-            expect(props.endLoading.calls.count()).toEqual(1);
-            done();
-        });
 
-        it("should ignore untouched fields when merging changed fields", async (done) => {
-            const props = getProps({
-                section: {
-                    update: 'test1'
-                },
-                entity: { foo: 'foo', bar: 'bar' }
-            });
-            const fields = {foo: '', bar: undefined, boo: 'boo'};
-
-            await tryUpdateData(fields, props);
-
-            expect(props.test1.calls.argsFor(0)[0]).toEqual({foo: '', bar: 'bar', boo: 'boo'});
-            expect(props.endEditing.calls.count()).toEqual(1);
-            expect(props.resetForm.calls.count()).toEqual(1);
-            expect(props.startLoading.calls.count()).toEqual(1);
-            expect(props.setError.calls.count()).toEqual(0);
-            expect(props.endLoading.calls.count()).toEqual(1);
-            done();
-        });
-    });
-
-    describe("tryUpdateFields()", () => {
-        it("should call update function with all updated fields", async (done) => {
-            const props = getProps();
-            const formFields = {
-                foo: {foo: undefined, bar: 'bar2'},
-                bar: {foo: '', bar: 'bar2'}
-            };
-
-            await tryUpdateFields(formFields, props);
-
-            expect(props.updateField.calls.argsFor(0)[0]).toEqual({foo: 'foo', bar: 'bar2'});
-            expect(props.updateField.calls.argsFor(1)[0]).toEqual({foo: '', bar: 'bar2'});
-            done();
-        });
-
-        it("should not call update function for items where all fields are untouched", async (done) => {
-            const props = getProps();
-            const formFields = {
-                foo: {foo: undefined, bar: undefined},
-                bar: {foo: undefined, bar: ''}
-            };
-
-            await tryUpdateFields(formFields, props);
-
-            expect(props.updateField.calls.argsFor(0)[0]).toEqual({foo: 'bar', bar: ''});
-            expect(props.updateField.calls.count()).toEqual(1);
-            done();
-        });
-    });
 
     describe("databaseToForeignKeys()", () => {
         it("should build foreignKey viewmodels from database", () => {
@@ -198,62 +63,7 @@ describe("Reference utils.js", () => {
         });
     });
 
-    describe("buildBreadcrumbs()", () => {
-        const section1 = {
-            id: 1,
-            breadcrumb: 'section1'
-        };
-
-        const section2 = {
-            id: 2,
-            breadcrumb: 'section2',
-            parent: section1
-        };
-
-        const section3 = {
-            id: 3,
-            breadcrumb: 'section3',
-            parent: section2
-        };
-
-        const section4 = {
-            id: 4,
-            breadcrumb: 'section4',
-            parent: section3
-        };
-
-        const section5 = {
-            id: 5,
-            breadcrumb: 'section5',
-            parent: section4
-        };
-
-        it("should build correct breadcrumbs from parent section", () => {
-            const breadcrumbs = buildBreadcrumbs(section1);
-            expect(breadcrumbs).toEqual([
-                [ 'section1' ]
-            ]);
-        });
-
-        it("should build correct breadcrumbs from child section", () => {
-            const breadcrumbs = buildBreadcrumbs(section3);
-            expect(breadcrumbs).toEqual([
-                [ 'section1', 1 ],
-                [ 'section2', 2 ],
-                [ 'section3' ]
-            ]);
-        });
-
-        it("should keep at most 3 highest level breadcrumbs", () => {
-            const breadcrumbs = buildBreadcrumbs(section5);
-            expect(breadcrumbs).toEqual([
-                [ 'section3', 3 ],
-                [ 'section4', 4 ],
-                [ 'section5' ]
-            ]);
-        });
-    });
-
+    
     describe("tablesToSchemaSeparatedTables()", () => {
         it("should add schema separator to appropriate locations", () => {
             const tables = {
diff --git a/package.json b/package.json
index 698562cf7d7..7d6388ff1cb 100644
--- a/package.json
+++ b/package.json
@@ -152,7 +152,7 @@
     "test": "yarn run test-jest && yarn run test-karma",
     "test-karma": "karma start frontend/test/karma.conf.js --single-run",
     "test-karma-watch": "karma start frontend/test/karma.conf.js --auto-watch --reporters nyan",
-    "test-jest": "jest",
+    "test-jest": "jest --maxWorkers=10",
     "test-jest-watch": "jest --watch",
     "test-e2e": "JASMINE_CONFIG_PATH=./frontend/test/e2e/support/jasmine.json jasmine",
     "test-e2e-dev": "./frontend/test/e2e-with-persistent-browser.js",
@@ -185,6 +185,11 @@
     ],
     "setupFiles": [
       "<rootDir>/frontend/test/metabase-bootstrap.js"
-    ]
+    ],
+    "globals": {
+      "ace": {},
+      "ga": {},
+      "document": {}
+    }
   }
-}
+}
\ No newline at end of file
-- 
GitLab