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

Fix issue with waitForAction, add documentation [ci e2e]

parent c1978827
No related branches found
No related tags found
No related merge requests found
......@@ -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;
......@@ -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])
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment