From ebb5500c15e1ab652e1ab5eb493207262c6d052d Mon Sep 17 00:00:00 2001 From: Kamil Mielnik <kamil@kamilmielnik.com> Date: Tue, 20 Feb 2024 13:29:31 +0700 Subject: [PATCH] Fix - Flaky QueryBuilder tests (#38860) * Use fetchMock.flush() to prevent tests from interfering with each other * Revert "Use fetchMock.flush() to prevent tests from interfering with each other" This reverts commit 03f1277cf2bf481ac36db212e15b4f29511a386b. * Add typing for requests in the redux store * Fix QueryBuilder flakes by introducing waitForLoadingRequests * Simplify interface * Add mocks for requests in redux store, update types * Simplify code * Increase timeout --- .../advanced_permissions/types.ts | 4 +-- .../metabase-enterprise/sandboxes/types.ts | 4 +-- frontend/src/metabase-types/store/index.ts | 3 +- .../metabase-types/store/mocks/requests.ts | 8 ++++++ .../src/metabase-types/store/mocks/state.ts | 2 ++ frontend/src/metabase-types/store/requests.ts | 19 ++++++++++++- frontend/src/metabase-types/store/state.ts | 2 ++ .../dashboard/actions/cards.unit.spec.ts | 1 - .../query_builder/containers/test-utils.tsx | 28 +++++++++++++++++-- 9 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 frontend/src/metabase-types/store/mocks/requests.ts diff --git a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts index 5600ef333d0..929f2061ebc 100644 --- a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts +++ b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/types.ts @@ -1,7 +1,7 @@ import type { DatabaseId, GroupId, Impersonation } from "metabase-types/api"; import type { PartialBy } from "metabase/core/types"; -import type { RequestState } from "metabase-types/store/requests"; +import type { RequestsState, RequestState } from "metabase-types/store"; import type { EnterpriseState } from "metabase-enterprise/shared/types"; import type { EnterpriseSharedState } from "metabase-enterprise/shared/reducer"; import type { AdvancedPermissionsState } from "./reducer"; @@ -9,7 +9,7 @@ import type { AdvancedPermissionsState } from "./reducer"; export type ImpersonationParams = { groupId: GroupId; databaseId: DatabaseId }; export interface AdvancedPermissionsStoreState extends EnterpriseState { - requests: { + requests: RequestsState & { plugins: { advancedPermissionsPlugin: { policies: Record<string, RequestState>; diff --git a/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts b/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts index a8e331ea8cb..cb232410b2d 100644 --- a/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts +++ b/enterprise/frontend/src/metabase-enterprise/sandboxes/types.ts @@ -1,12 +1,12 @@ import type { EnterpriseSharedState } from "metabase-enterprise/shared/reducer"; import type { EnterpriseState } from "metabase-enterprise/shared/types"; import type { GroupTableAccessPolicy } from "metabase-types/api"; -import type { RequestState } from "metabase-types/store/requests"; +import type { RequestsState, RequestState } from "metabase-types/store"; export type GroupTableAccessPolicyParams = { groupId: string; tableId: string }; export interface SandboxesState extends EnterpriseState { - requests: { + requests: RequestsState & { plugins: { sandboxesPlugin: { policies: Record<string, RequestState>; diff --git a/frontend/src/metabase-types/store/index.ts b/frontend/src/metabase-types/store/index.ts index 4bce48841c9..da52d6d1589 100644 --- a/frontend/src/metabase-types/store/index.ts +++ b/frontend/src/metabase-types/store/index.ts @@ -6,7 +6,8 @@ export * from "./dashboard"; export * from "./embed"; export * from "./entities"; export * from "./metabot"; -export * from "./settings"; export * from "./qb"; +export * from "./requests"; +export * from "./settings"; export * from "./setup"; export * from "./state"; diff --git a/frontend/src/metabase-types/store/mocks/requests.ts b/frontend/src/metabase-types/store/mocks/requests.ts new file mode 100644 index 00000000000..70bba8a0145 --- /dev/null +++ b/frontend/src/metabase-types/store/mocks/requests.ts @@ -0,0 +1,8 @@ +import type { RequestsState } from "../requests"; + +export const createMockRequestsState = (): RequestsState => { + return { + entities: {}, + plugins: {}, + }; +}; diff --git a/frontend/src/metabase-types/store/mocks/state.ts b/frontend/src/metabase-types/store/mocks/state.ts index 2e12f7df64e..96e324120fc 100644 --- a/frontend/src/metabase-types/store/mocks/state.ts +++ b/frontend/src/metabase-types/store/mocks/state.ts @@ -9,6 +9,7 @@ import { createMockEmbedState } from "./embed"; import { createMockMetabotState } from "./metabot"; import { createMockParametersState } from "./parameters"; import { createMockQueryBuilderState } from "./qb"; +import { createMockRequestsState } from "./requests"; import { createMockRoutingState } from "./routing"; import { createMockSettingsState } from "./settings"; import { createMockSetupState } from "./setup"; @@ -28,6 +29,7 @@ export const createMockState = ( metabot: createMockMetabotState(), parameters: createMockParametersState(), qb: createMockQueryBuilderState(), + requests: createMockRequestsState(), routing: createMockRoutingState(), settings: createMockSettingsState(), setup: createMockSetupState(), diff --git a/frontend/src/metabase-types/store/requests.ts b/frontend/src/metabase-types/store/requests.ts index 5a0770611ed..4a85f7c193a 100644 --- a/frontend/src/metabase-types/store/requests.ts +++ b/frontend/src/metabase-types/store/requests.ts @@ -1,7 +1,24 @@ +type EntityKey = string; + +type QueryKey = string; + +type RequestType = string; // only "fetch"? + +// See initialRequestState in metabase/redux/requests.js export interface RequestState { loading: boolean; loaded: boolean; fetched: boolean; - error: any | null; + error: unknown | null; _isRequestState: true; } + +type RequestsGroupState = Record< + EntityKey, + Record<QueryKey, Record<RequestType, RequestState>> +>; + +export type RequestsState = { + plugins: RequestsGroupState; + entities: RequestsGroupState; +}; diff --git a/frontend/src/metabase-types/store/state.ts b/frontend/src/metabase-types/store/state.ts index 6cd47a440ad..6aebf59cf2c 100644 --- a/frontend/src/metabase-types/store/state.ts +++ b/frontend/src/metabase-types/store/state.ts @@ -9,6 +9,7 @@ import type { EntitiesState } from "./entities"; import type { MetabotState } from "./metabot"; import type { QueryBuilderState } from "./qb"; import type { ParametersState } from "./parameters"; +import type { RequestsState } from "./requests"; import type { SettingsState } from "./settings"; import type { SetupState } from "./setup"; import type { FileUploadState } from "./upload"; @@ -25,6 +26,7 @@ export interface State { metabot: MetabotState; parameters: ParametersState; qb: QueryBuilderState; + requests: RequestsState; routing: RouterState; settings: SettingsState; setup: SetupState; diff --git a/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts b/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts index 001968043fa..e9a8e3297ef 100644 --- a/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts +++ b/frontend/src/metabase/dashboard/actions/cards.unit.spec.ts @@ -164,7 +164,6 @@ function setup({ dashcards: _.indexBy(dashcards, "id"), }); - // @ts-expect-error we need better redux test tooling const store = getStore( mainReducers, createMockState({ dashboard: dashboardState }), diff --git a/frontend/src/metabase/query_builder/containers/test-utils.tsx b/frontend/src/metabase/query_builder/containers/test-utils.tsx index 3f73a10d7b8..a177ad67d17 100644 --- a/frontend/src/metabase/query_builder/containers/test-utils.tsx +++ b/frontend/src/metabase/query_builder/containers/test-utils.tsx @@ -45,6 +45,7 @@ import { SAMPLE_DB_ID, createSampleDatabase, } from "metabase-types/api/mocks/presets"; +import type { State, RequestState } from "metabase-types/store"; import NewItemMenu from "metabase/containers/NewItemMenu"; import { LOAD_COMPLETE_FAVICON } from "metabase/hoc/Favicon"; import { serializeCardForUrl } from "metabase/lib/card"; @@ -252,7 +253,11 @@ export const setup = async ({ const mockEventListener = jest.spyOn(window, "addEventListener"); - const { container, history } = renderWithProviders( + const { + store: { getState }, + container, + history, + } = renderWithProviders( <Route> <Route path="/" component={TestHome} /> <Route path="/model"> @@ -278,7 +283,7 @@ export const setup = async ({ }, ); - await waitForLoaderToBeRemoved(); + await waitForLoadingRequests(getState); return { container, @@ -287,6 +292,25 @@ export const setup = async ({ }; }; +const waitForLoadingRequests = async (getState: () => State) => { + await waitFor( + () => { + const requests = getRequests(getState()); + const areRequestsLoading = requests.some(request => request.loading); + expect(areRequestsLoading).toBe(false); + }, + { timeout: 5000 }, + ); +}; + +const getRequests = (state: State): RequestState[] => { + return Object.values(state.requests).flatMap(group => + Object.values(group).flatMap(entity => + Object.values(entity).flatMap(request => Object.values(request)), + ), + ); +}; + export const startNewNotebookModel = async () => { userEvent.click(screen.getByText("Use the notebook editor")); await waitForLoaderToBeRemoved(); -- GitLab