diff --git a/enterprise/frontend/src/metabase-enterprise/auth/components/SettingsSAMLForm.jsx b/enterprise/frontend/src/metabase-enterprise/auth/components/SettingsSAMLForm.jsx index bdd4f183af4587ee121bccbac6c21728425a18e1..1aa16b434a52e0129b853b13068586416861614f 100644 --- a/enterprise/frontend/src/metabase-enterprise/auth/components/SettingsSAMLForm.jsx +++ b/enterprise/frontend/src/metabase-enterprise/auth/components/SettingsSAMLForm.jsx @@ -12,7 +12,7 @@ import Form, { FormSubmit, FormMessage, FormSection, -} from "metabase/containers/Form"; +} from "metabase/containers/FormikForm"; import Breadcrumbs from "metabase/components/Breadcrumbs"; import CopyWidget from "metabase/components/CopyWidget"; diff --git a/frontend/src/metabase/admin/settings/components/SettingsGoogleForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsGoogleForm.jsx index 25a681a45b0d5283c5c0b2460045b6e8009c2fa5..481e370ba346f4f77ed20892b07864ddbbb41853 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsGoogleForm.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsGoogleForm.jsx @@ -8,7 +8,7 @@ import Form, { FormField, FormSubmit, FormMessage, -} from "metabase/containers/Form"; +} from "metabase/containers/FormikForm"; import { updateSettings } from "metabase/admin/settings/settings"; import { settingToFormField } from "metabase/admin/settings/utils"; @@ -38,6 +38,7 @@ class SettingsGoogleForm extends Component { style={{ maxWidth: 520 }} initialValues={initialValues} onSubmit={updateSettings} + overwriteOnInitialValuesChange > <Breadcrumbs crumbs={[ diff --git a/frontend/src/metabase/admin/settings/slack/components/SlackSetupForm/SlackSetupForm.tsx b/frontend/src/metabase/admin/settings/slack/components/SlackSetupForm/SlackSetupForm.tsx index c439a5317c2331a1afac50eaa9e9df1a691b6888..a8f7fd68750b2c2e5e8e3f71dacf8a683eee6df4 100644 --- a/frontend/src/metabase/admin/settings/slack/components/SlackSetupForm/SlackSetupForm.tsx +++ b/frontend/src/metabase/admin/settings/slack/components/SlackSetupForm/SlackSetupForm.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; import { t } from "ttag"; -import Form from "metabase/containers/Form"; +import Form from "metabase/containers/FormikForm"; import { SlackSettings } from "metabase-types/api"; import { getSlackForm } from "../../forms"; import { FormProps } from "./types"; diff --git a/frontend/src/metabase/admin/settings/slack/components/SlackStatusForm/SlackStatusForm.tsx b/frontend/src/metabase/admin/settings/slack/components/SlackStatusForm/SlackStatusForm.tsx index eb1d2620b399a6421fe9a036ed3556fce114a4bc..5fc02da7573119846b89c92d6453dad36065b20d 100644 --- a/frontend/src/metabase/admin/settings/slack/components/SlackStatusForm/SlackStatusForm.tsx +++ b/frontend/src/metabase/admin/settings/slack/components/SlackStatusForm/SlackStatusForm.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from "react"; -import Form from "metabase/containers/Form"; +import Form from "metabase/containers/FormikForm"; import { SlackSettings } from "metabase-types/api"; import { getSlackForm } from "../../forms"; import { FormProps } from "./types"; @@ -13,7 +13,11 @@ const SlackStatusForm = ({ settings }: SlackStatusFormProps): JSX.Element => { const onSubmit = useCallback(() => undefined, []); return ( - <Form form={form} initialValues={settings} onSubmit={onSubmit}> + <Form<SlackSettings> + form={form} + initialValues={settings} + onSubmit={onSubmit} + > {({ Form, FormField }: FormProps) => ( <Form> <FormField name="slack-app-token" /> diff --git a/frontend/src/metabase/admin/settings/slack/forms.ts b/frontend/src/metabase/admin/settings/slack/forms.ts index 35bab5bea5a9d2290b8b511906ef8ab624e3dc25..e4f83df2741728d10309bb632a5181d33df10779 100644 --- a/frontend/src/metabase/admin/settings/slack/forms.ts +++ b/frontend/src/metabase/admin/settings/slack/forms.ts @@ -1,6 +1,10 @@ import { t } from "ttag"; +import { SlackSettings } from "metabase-types/api"; +import { FormObject } from "metabase-types/forms"; -export const getSlackForm = (readOnly?: boolean) => ({ +export const getSlackForm = ( + readOnly?: boolean, +): FormObject<SlackSettings> => ({ fields: [ { name: "slack-app-token", diff --git a/frontend/src/metabase/containers/SaveQuestionModal.jsx b/frontend/src/metabase/containers/SaveQuestionModal.jsx index 0d1eefceb143b97ed477ed2dcc59d6a5c9c1ab13..2bd19af3f8b46a8c85e016c94b97beade241ea15 100644 --- a/frontend/src/metabase/containers/SaveQuestionModal.jsx +++ b/frontend/src/metabase/containers/SaveQuestionModal.jsx @@ -4,7 +4,7 @@ import PropTypes from "prop-types"; import { CSSTransitionGroup } from "react-transition-group"; import { t } from "ttag"; -import Form, { FormField, FormFooter } from "metabase/containers/Form"; +import Form, { FormField, FormFooter } from "metabase/containers/FormikForm"; import ModalContent from "metabase/components/ModalContent"; import Radio from "metabase/core/components/Radio"; import * as Q_DEPRECATED from "metabase/lib/query"; @@ -25,8 +25,8 @@ export default class SaveQuestionModal extends Component { multiStep: PropTypes.bool, }; - validateName = (name, context) => { - if (context.form.saveType.value !== "overwrite") { + validateName = (name, { values }) => { + if (values.saveType !== "overwrite") { // We don't care if the form is valid when overwrite mode is enabled, // as original question's data will be submitted instead of the form values return validate.required()(name); diff --git a/frontend/src/metabase/entities/questions/forms.js b/frontend/src/metabase/entities/questions/forms.js index a147f00255d392450b9538a2c17f34c81de058c4..748952f403b57a1b22052b8b939f97ab71005eed 100644 --- a/frontend/src/metabase/entities/questions/forms.js +++ b/frontend/src/metabase/entities/questions/forms.js @@ -1,9 +1,12 @@ import { t } from "ttag"; + import MetabaseSettings from "metabase/lib/settings"; +import validate from "metabase/lib/validate"; + import { PLUGIN_CACHING } from "metabase/plugins"; const FORM_FIELDS = [ - { name: "name", title: t`Name` }, + { name: "name", title: t`Name`, validate: validate.required() }, { name: "description", title: t`Description`, diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetFieldMetadataSidebar/DatasetFieldMetadataSidebar.jsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetFieldMetadataSidebar/DatasetFieldMetadataSidebar.jsx index b6222c5c92df0727fcd71e104d7712de7bad949f..ad261db91904af8fc4ac0e2415bd7e3b2e51c001 100644 --- a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetFieldMetadataSidebar/DatasetFieldMetadataSidebar.jsx +++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetFieldMetadataSidebar/DatasetFieldMetadataSidebar.jsx @@ -18,7 +18,7 @@ import { import { isLocalField, isSameField } from "metabase/lib/query/field_ref"; import { isFK, getSemanticTypeIcon } from "metabase/lib/schema_metadata"; -import RootForm from "metabase/containers/Form"; +import RootForm from "metabase/containers/FormikForm"; import { usePrevious } from "metabase/hooks/use-previous"; import SidebarContent from "metabase/query_builder/components/SidebarContent"; @@ -76,7 +76,7 @@ function getFormFields({ dataset }) { value: type.id, })); - return fieldFormValues => + return formFieldValues => [ { name: "display_name", title: t`Display name` }, { @@ -96,11 +96,11 @@ function getFormFields({ dataset }) { title: t`Column type`, widget: SemanticTypePicker, options: getSemanticTypeOptions(), - icon: getSemanticTypeIcon(fieldFormValues.semantic_type, "ellipsis"), + icon: getSemanticTypeIcon(formFieldValues?.semantic_type, "ellipsis"), }, { name: "fk_target_field_id", - hidden: !isFK(fieldFormValues), + hidden: !isFK(formFieldValues), widget: FKTargetPicker, databaseId: dataset.databaseId(), }, diff --git a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx index 5f8ead6136ce5c0b74b832d7df1a5e810704d74d..6a8b52c8fb0951040d4fe450b01d20b88bd8437e 100644 --- a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx +++ b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { t } from "ttag"; import Users from "metabase/entities/users"; -import Form from "metabase/containers/Form"; +import Form from "metabase/containers/FormikForm"; import { SubscribeInfo } from "metabase-types/store"; import { FormContainer, @@ -47,7 +47,7 @@ const NewsletterForm = ({ {t`Get infrequent emails about new releases and feature updates.`} </FormHeader> {!isSubscribed && ( - <Form + <Form<{ email: string }> form={Users.forms.newsletter} initialValues={initialValues} submitTitle={t`Subscribe`} diff --git a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx index 326786a1567ab4f65bbe1c26b506bf3e089cc464..72e3d88017e5360f71c63ebbd137aef9ce8f3337 100644 --- a/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx +++ b/frontend/src/metabase/setup/components/NewsletterForm/NewsletterForm.unit.spec.tsx @@ -9,7 +9,7 @@ const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => ( </form> ); -jest.mock("metabase/containers/Form", () => FormMock); +jest.mock("metabase/containers/FormikForm", () => FormMock); jest.mock("metabase/entities/users", () => ({ forms: { newsletter: jest.fn() }, diff --git a/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.tsx index 6df46fadbe1136e1ada865905323cdd64d309fa6..160fccfebce532e7da9f91f37dde54b16706a403 100644 --- a/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.tsx +++ b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo } from "react"; import { t } from "ttag"; -import Form from "metabase/containers/Form"; +import Form from "metabase/containers/FormikForm"; import forms from "metabase/entities/timeline-events/forms"; import { Timeline, TimelineEvent } from "metabase-types/api"; import ModalBody from "../ModalBody"; @@ -47,7 +47,7 @@ const EditEventModal = ({ <div> <ModalHeader title={t`Edit event`} onClose={onClose} /> <ModalBody> - <Form + <Form<TimelineEvent> form={form} initialValues={event} isModal={true} diff --git a/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.unit.spec.tsx index 8bad3d3ed005155474c5e86d23490ccf9c6f57ee..4bbc5d3cdd925c68c5d65c4f04d08f83ae63403d 100644 --- a/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.unit.spec.tsx +++ b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.unit.spec.tsx @@ -13,7 +13,7 @@ const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => ( </form> ); -jest.mock("metabase/containers/Form", () => FormMock); +jest.mock("metabase/containers/FormikForm", () => FormMock); describe("EditEventModal", () => { it("should submit modal", () => { diff --git a/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.tsx b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.tsx index ddf898d621e9eeb9e00aa515275157e2f9110d53..d2b667cb333553e8f0acb8aaf25519c02b478792 100644 --- a/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.tsx +++ b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo } from "react"; import { t } from "ttag"; -import Form from "metabase/containers/Form"; +import Form from "metabase/containers/FormikForm"; import forms from "metabase/entities/timelines/forms"; import { Timeline } from "metabase-types/api"; import ModalBody from "../ModalBody"; diff --git a/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx index 6a79e56ea25bdbf8627bf8ea327f8c67d289ed0b..1922da2da09b806f3275eb76e48d4ca5c35e18e4 100644 --- a/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx +++ b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx @@ -10,7 +10,7 @@ const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => ( </form> ); -jest.mock("metabase/containers/Form", () => FormMock); +jest.mock("metabase/containers/FormikForm", () => FormMock); describe("EditTimelineModal", () => { it("should submit modal", () => { diff --git a/frontend/src/metabase/timelines/common/components/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/common/components/NewEventModal/NewEventModal.tsx index 41c8fb917085be02ca7d21d257c99d56517a6e6d..f431c5f2abb1f34feb2a1fe81ce47bd0508ec405 100644 --- a/frontend/src/metabase/timelines/common/components/NewEventModal/NewEventModal.tsx +++ b/frontend/src/metabase/timelines/common/components/NewEventModal/NewEventModal.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo } from "react"; import { t } from "ttag"; import { getDefaultTimezone } from "metabase/lib/time"; import { getDefaultTimelineIcon } from "metabase/lib/timelines"; -import Form from "metabase/containers/Form"; +import Form from "metabase/containers/FormikForm"; import forms from "metabase/entities/timeline-events/forms"; import { Collection, @@ -51,7 +51,7 @@ const NewEventModal = ({ const hasOneTimeline = availableTimelines.length === 1; return { - timeline_id: defaultTimeline ? defaultTimeline.id : null, + timeline_id: defaultTimeline ? defaultTimeline.id : undefined, icon: hasOneTimeline ? defaultTimeline.icon : getDefaultTimelineIcon(), timezone: getDefaultTimezone(), source, @@ -73,7 +73,7 @@ const NewEventModal = ({ <div> <ModalHeader title={t`New event`} onClose={onClose} /> <ModalBody> - <Form + <Form<Partial<TimelineEvent>> form={form} initialValues={initialValues} isModal={true} diff --git a/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.tsx b/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.tsx index a614269beb30a21a6afe0799e750b096ed872056..38ff14869839f01458241eda9cb641b2e745a314 100644 --- a/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.tsx +++ b/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo } from "react"; import { t } from "ttag"; -import Form from "metabase/containers/Form"; +import Form from "metabase/containers/FormikForm"; import forms from "metabase/entities/timelines/forms"; import { getDefaultTimelineIcon } from "metabase/lib/timelines"; import { canonicalCollectionId } from "metabase/collections/utils"; diff --git a/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx index 5cb844e46dcc434039810a3d44b5e6238052fabf..9213abc16b6011b12a61941f2d974ef969664ec1 100644 --- a/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx +++ b/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx @@ -10,7 +10,7 @@ const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => ( </form> ); -jest.mock("metabase/containers/Form", () => FormMock); +jest.mock("metabase/containers/FormikForm", () => FormMock); describe("NewTimelineModal", () => { it("should submit modal", () => { diff --git a/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js b/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js index b77615c8d7c71a1106306642206a88d806d95eac..c4014a42c29e3f8873bf2fd935dc2c20f157d573 100644 --- a/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js +++ b/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js @@ -1,5 +1,5 @@ import React from "react"; -import { renderWithProviders, screen } from "__support__/ui"; +import { act, renderWithProviders, screen, waitFor } from "__support__/ui"; import userEvent from "@testing-library/user-event"; import mock from "xhr-mock"; @@ -37,7 +37,7 @@ function mockCachingEnabled(enabled = true) { }); } -const renderSaveQuestionModal = (question, originalQuestion) => { +const setup = async (question, originalQuestion) => { const onCreateMock = jest.fn(() => Promise.resolve()); const onSaveMock = jest.fn(() => Promise.resolve()); const onCloseMock = jest.fn(); @@ -51,6 +51,7 @@ const renderSaveQuestionModal = (question, originalQuestion) => { onClose={onCloseMock} />, ); + await waitFor(() => screen.getByRole("button", { name: "Save" })); return { onSaveMock, onCreateMock, onCloseMock }; }; @@ -60,7 +61,7 @@ function getQuestion({ isSaved, name = "Q1", description = "Example", - collection_id = 12, + collection_id = null, can_write = true, } = {}) { const extraCardParams = {}; @@ -106,20 +107,25 @@ function getDirtyQuestion(originalQuestion) { }); } -function fillForm({ name, description }) { +async function fillForm({ name, description }) { if (name) { const input = screen.getByLabelText("Name"); - userEvent.clear(input); - userEvent.type(input, name); + await userEvent.clear(input); + await userEvent.type(input, name); } if (description) { const input = screen.getByLabelText("Description"); - userEvent.clear(input); - userEvent.type(input, description); + await userEvent.clear(input); + await userEvent.type(input, description); } } describe("SaveQuestionModal", () => { + beforeAll(() => { + console.error = jest.fn(); + console.warn = jest.fn(); + }); + const TEST_COLLECTIONS = [ { can_write: false, @@ -148,6 +154,9 @@ describe("SaveQuestionModal", () => { mock.get("/api/collection", { body: JSON.stringify(TEST_COLLECTIONS), }); + mock.get("/api/collection/root", { + body: JSON.stringify(TEST_COLLECTIONS)[0], + }); }); afterEach(() => { @@ -155,15 +164,15 @@ describe("SaveQuestionModal", () => { }); describe("new question", () => { - it("should suggest a name for structured queries", () => { - renderSaveQuestionModal(getQuestion()); + it("should suggest a name for structured queries", async () => { + await setup(getQuestion()); expect(screen.getByLabelText("Name")).toHaveValue( EXPECTED_SUGGESTED_NAME, ); }); - it("should not suggest a name for native queries", () => { - renderSaveQuestionModal( + it("should not suggest a name for native queries", async () => { + await setup( new Question( { dataset_query: { @@ -182,16 +191,18 @@ describe("SaveQuestionModal", () => { expect(screen.getByLabelText("Name")).toHaveValue(""); }); - it("should display empty description input", () => { - renderSaveQuestionModal(getQuestion()); + it("should display empty description input", async () => { + await setup(getQuestion()); expect(screen.getByLabelText("Description")).toHaveValue(""); }); - it("should call onCreate correctly with default form values", () => { + it("should call onCreate correctly with default form values", async () => { const question = getQuestion(); - const { onCreateMock } = renderSaveQuestionModal(question); + const { onCreateMock } = await setup(question); - userEvent.click(screen.getByText("Save")); + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onCreateMock).toHaveBeenCalledTimes(1); expect(onCreateMock).toHaveBeenCalledWith({ @@ -202,12 +213,17 @@ describe("SaveQuestionModal", () => { }); }); - it("should call onCreate correctly with edited form", () => { + it("should call onCreate correctly with edited form", async () => { const question = getQuestion(); - const { onCreateMock } = renderSaveQuestionModal(question); - - fillForm({ name: "My favorite orders", description: "So many of them" }); - userEvent.click(screen.getByText("Save")); + const { onCreateMock } = await setup(question); + + await act(async () => { + await fillForm({ + name: "My favorite orders", + description: "So many of them", + }); + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onCreateMock).toHaveBeenCalledTimes(1); expect(onCreateMock).toHaveBeenCalledWith({ @@ -218,15 +234,17 @@ describe("SaveQuestionModal", () => { }); }); - it("should trim name and description", () => { + it("should trim name and description", async () => { const question = getQuestion(); - const { onCreateMock } = renderSaveQuestionModal(question); - - fillForm({ - name: " My favorite orders ", - description: " So many of them ", + const { onCreateMock } = await setup(question); + + await act(async () => { + await fillForm({ + name: " My favorite orders ", + description: " So many of them ", + }); + await userEvent.click(screen.getByRole("button", { name: "Save" })); }); - userEvent.click(screen.getByText("Save")); expect(onCreateMock).toHaveBeenCalledTimes(1); expect(onCreateMock).toHaveBeenCalledWith({ @@ -237,14 +255,16 @@ describe("SaveQuestionModal", () => { }); }); - it('should correctly handle saving a question in the "root" collection', () => { + it('should correctly handle saving a question in the "root" collection', async () => { const question = getQuestion({ collection_id: "root", }); - const { onCreateMock } = renderSaveQuestionModal(question); + const { onCreateMock } = await setup(question); - fillForm({ name: "foo", description: "bar" }); - userEvent.click(screen.getByText("Save")); + await act(async () => { + await fillForm({ name: "foo", description: "bar" }); + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onCreateMock).toHaveBeenCalledTimes(1); expect(onCreateMock).toHaveBeenCalledWith({ @@ -255,17 +275,19 @@ describe("SaveQuestionModal", () => { }); }); - it("shouldn't call onSave when form is submitted", () => { + it("shouldn't call onSave when form is submitted", async () => { const question = getQuestion(); - const { onSaveMock } = renderSaveQuestionModal(question); + const { onSaveMock } = await setup(question); - userEvent.click(screen.getByText("Save")); + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onSaveMock).not.toHaveBeenCalled(); }); - it("shouldn't show a control to overwrite a saved question", () => { - renderSaveQuestionModal(getQuestion()); + it("shouldn't show a control to overwrite a saved question", async () => { + await setup(getQuestion()); expect( screen.queryByText("Save as new question"), ).not.toBeInTheDocument(); @@ -276,12 +298,9 @@ describe("SaveQuestionModal", () => { }); describe("saving as a new question", () => { - it("should offer to replace the original question by default", () => { + it("should offer to replace the original question by default", async () => { const originalQuestion = getQuestion({ isSaved: true }); - renderSaveQuestionModal( - getDirtyQuestion(originalQuestion), - originalQuestion, - ); + await setup(getDirtyQuestion(originalQuestion), originalQuestion); expect( screen.getByLabelText(/Replace original question, ".*"/), @@ -289,7 +308,7 @@ describe("SaveQuestionModal", () => { expect(screen.getByText("Save as new question")).not.toBeChecked(); }); - it("should switch to the new question form", () => { + it("should switch to the new question form", async () => { const CARD = { name: "Q1", description: "Example description", @@ -297,9 +316,11 @@ describe("SaveQuestionModal", () => { }; const originalQuestion = getQuestion({ isSaved: true, ...CARD }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - renderSaveQuestionModal(dirtyQuestion, originalQuestion); + await setup(dirtyQuestion, originalQuestion); - userEvent.click(screen.getByText("Save as new question")); + await act(async () => { + await userEvent.click(screen.getByText("Save as new question")); + }); expect(screen.getByLabelText("Name")).toHaveValue( EXPECTED_DIRTY_SUGGESTED_NAME, @@ -310,16 +331,16 @@ describe("SaveQuestionModal", () => { expect(screen.queryByText("Our analytics")).toBeInTheDocument(); }); - it("should allow to save a question with default form values", () => { + // one + it("should allow to save a question with default form values", async () => { const originalQuestion = getQuestion({ isSaved: true }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - const { onCreateMock } = renderSaveQuestionModal( - dirtyQuestion, - originalQuestion, - ); + const { onCreateMock } = await setup(dirtyQuestion, originalQuestion); - userEvent.click(screen.getByText("Save as new question")); - userEvent.click(screen.getByRole("button", { name: "Save" })); + await act(async () => { + await userEvent.click(screen.getByText("Save as new question")); + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onCreateMock).toHaveBeenCalledTimes(1); expect(onCreateMock).toHaveBeenCalledWith({ @@ -328,17 +349,16 @@ describe("SaveQuestionModal", () => { }); }); - it("show allow to save a question with an edited form", () => { + it("show allow to save a question with an edited form", async () => { const originalQuestion = getQuestion({ isSaved: true }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - const { onCreateMock } = renderSaveQuestionModal( - dirtyQuestion, - originalQuestion, - ); + const { onCreateMock } = await setup(dirtyQuestion, originalQuestion); - userEvent.click(screen.getByText("Save as new question")); - fillForm({ name: "My Q", description: "Sample" }); - userEvent.click(screen.getByRole("button", { name: "Save" })); + await act(async () => { + await userEvent.click(screen.getByText("Save as new question")); + await fillForm({ name: "My Q", description: "Sample" }); + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onCreateMock).toHaveBeenCalledTimes(1); expect(onCreateMock).toHaveBeenCalledWith({ @@ -348,44 +368,42 @@ describe("SaveQuestionModal", () => { }); }); - it("shouldn't allow to save a question if form is invalid", () => { + it("shouldn't allow to save a question if form is invalid", async () => { const originalQuestion = getQuestion({ isSaved: true }); - renderSaveQuestionModal( - getDirtyQuestion(originalQuestion), - originalQuestion, - ); + await setup(getDirtyQuestion(originalQuestion), originalQuestion); - userEvent.click(screen.getByText("Save as new question")); - userEvent.clear(screen.getByLabelText("Name")); - userEvent.clear(screen.getByLabelText("Description")); + await act(async () => { + await userEvent.click(screen.getByText("Save as new question")); + await userEvent.clear(screen.getByLabelText("Name")); + await userEvent.clear(screen.getByLabelText("Description")); + }); expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); }); }); describe("overwriting a saved question", () => { - it("should display original question's name on save mode control", () => { + it("should display original question's name on save mode control", async () => { const originalQuestion = getQuestion({ isSaved: true, name: "Beautiful Orders", }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - renderSaveQuestionModal(dirtyQuestion, originalQuestion); + await setup(dirtyQuestion, originalQuestion); expect( screen.queryByText('Replace original question, "Beautiful Orders"'), ).toBeInTheDocument(); }); - it("should call onSave correctly when form is submitted", () => { + it("should call onSave correctly when form is submitted", async () => { const originalQuestion = getQuestion({ isSaved: true }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - const { onSaveMock } = renderSaveQuestionModal( - dirtyQuestion, - originalQuestion, - ); + const { onSaveMock } = await setup(dirtyQuestion, originalQuestion); - userEvent.click(screen.getByText("Save")); + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onSaveMock).toHaveBeenCalledTimes(1); expect(onSaveMock).toHaveBeenCalledWith({ @@ -394,17 +412,18 @@ describe("SaveQuestionModal", () => { }); }); - it("should allow switching to 'save as new' and back", () => { + it("should allow switching to 'save as new' and back", async () => { const originalQuestion = getQuestion({ isSaved: true }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - const { onSaveMock } = renderSaveQuestionModal( - dirtyQuestion, - originalQuestion, - ); - - userEvent.click(screen.getByText("Save as new question")); - userEvent.click(screen.getByText(/Replace original question, ".*"/)); - userEvent.click(screen.getByText("Save")); + const { onSaveMock } = await setup(dirtyQuestion, originalQuestion); + + await act(async () => { + await userEvent.click(screen.getByText("Save as new question")); + await userEvent.click( + screen.getByText(/Replace original question, ".*"/), + ); + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onSaveMock).toHaveBeenCalledTimes(1); expect(onSaveMock).toHaveBeenCalledWith({ @@ -413,17 +432,19 @@ describe("SaveQuestionModal", () => { }); }); - it("should preserve original question's collection id", () => { + it("should preserve original question's collection id", async () => { const originalQuestion = getQuestion({ isSaved: true, collection_id: 5, }); - const { onSaveMock } = renderSaveQuestionModal( + const { onSaveMock } = await setup( getDirtyQuestion(originalQuestion), originalQuestion, ); - userEvent.click(screen.getByText("Save")); + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onSaveMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -432,42 +453,44 @@ describe("SaveQuestionModal", () => { ); }); - it("shouldn't allow to save a question if form is invalid", () => { - renderSaveQuestionModal(getQuestion()); + it("shouldn't allow to save a question if form is invalid", async () => { + await setup(getQuestion()); - userEvent.clear(screen.getByLabelText("Name")); - userEvent.clear(screen.getByLabelText("Description")); + await act(async () => { + await userEvent.clear(screen.getByLabelText("Name")); + await userEvent.clear(screen.getByLabelText("Description")); + }); expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); }); - it("shouldn't call onCreate when form is submitted", () => { + it("shouldn't call onCreate when form is submitted", async () => { const originalQuestion = getQuestion({ isSaved: true }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - const { onCreateMock } = renderSaveQuestionModal( - dirtyQuestion, - originalQuestion, - ); + const { onCreateMock } = await setup(dirtyQuestion, originalQuestion); - userEvent.click(screen.getByText("Save")); + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "Save" })); + }); expect(onCreateMock).not.toHaveBeenCalled(); }); - it("should keep 'save as new' form values while switching saving modes", () => { + it("should keep 'save as new' form values while switching saving modes", async () => { const originalQuestion = getQuestion({ isSaved: true }); - renderSaveQuestionModal( - getDirtyQuestion(originalQuestion), - originalQuestion, - ); - - userEvent.click(screen.getByText("Save as new question")); - fillForm({ - name: "Should not be erased", - description: "This should not be erased too", + await setup(getDirtyQuestion(originalQuestion), originalQuestion); + + await act(async () => { + await userEvent.click(screen.getByText("Save as new question")); + await fillForm({ + name: "Should not be erased", + description: "This should not be erased too", + }); + await userEvent.click( + screen.getByText(/Replace original question, ".*"/), + ); + await userEvent.click(screen.getByText("Save as new question")); }); - userEvent.click(screen.getByText(/Replace original question, ".*"/)); - userEvent.click(screen.getByText("Save as new question")); expect(screen.getByLabelText("Name")).toHaveValue("Should not be erased"); expect(screen.getByLabelText("Description")).toHaveValue( @@ -475,28 +498,29 @@ describe("SaveQuestionModal", () => { ); }); - it("should allow to replace the question if new question form is invalid (metabase#13817", () => { + it("should allow to replace the question if new question form is invalid (metabase#13817)", async () => { const originalQuestion = getQuestion({ isSaved: true }); - renderSaveQuestionModal( - getDirtyQuestion(originalQuestion), - originalQuestion, - ); - - userEvent.click(screen.getByText("Save as new question")); - userEvent.clear(screen.getByLabelText("Name")); - userEvent.click(screen.getByText(/Replace original question, ".*"/)); + await setup(getDirtyQuestion(originalQuestion), originalQuestion); + + await act(async () => { + await userEvent.click(screen.getByText("Save as new question")); + await userEvent.clear(screen.getByLabelText("Name")); + await userEvent.click( + screen.getByText(/Replace original question, ".*"/), + ); + }); - expect(screen.getByRole("button", { name: "Save" })).toBeEnabled(); + expect(await screen.getByRole("button", { name: "Save" })).toBeEnabled(); }); - it("should not allow overwriting when user does not have curate permission on collection (metabase#20717)", () => { + it("should not allow overwriting when user does not have curate permission on collection (metabase#20717)", async () => { const originalQuestion = getQuestion({ isSaved: true, name: "Beautiful Orders", can_write: false, }); const dirtyQuestion = getDirtyQuestion(originalQuestion); - renderSaveQuestionModal(dirtyQuestion, originalQuestion); + await setup(dirtyQuestion, originalQuestion); expect( screen.queryByText("Save as new question"), @@ -507,15 +531,19 @@ describe("SaveQuestionModal", () => { }); }); - it("should call onClose when Cancel button is clicked", () => { - const { onCloseMock } = renderSaveQuestionModal(getQuestion()); - userEvent.click(screen.getByRole("button", { name: "Cancel" })); + it("should call onClose when Cancel button is clicked", async () => { + const { onCloseMock } = await setup(getQuestion()); + await act(async () => { + userEvent.click(screen.getByRole("button", { name: "Cancel" })); + }); expect(onCloseMock).toHaveBeenCalledTimes(1); }); - it("should call onClose when close icon is clicked", () => { - const { onCloseMock } = renderSaveQuestionModal(getQuestion()); - userEvent.click(screen.getByLabelText("close icon")); + it("should call onClose when close icon is clicked", async () => { + const { onCloseMock } = await setup(getQuestion()); + await act(async () => { + userEvent.click(screen.getByLabelText("close icon")); + }); expect(onCloseMock).toHaveBeenCalledTimes(1); }); @@ -534,8 +562,8 @@ describe("SaveQuestionModal", () => { .question(); describe("OSS", () => { - it("is not shown", () => { - renderSaveQuestionModal(question); + it("is not shown", async () => { + await setup(question); expect(screen.queryByText("More options")).not.toBeInTheDocument(); expect( screen.queryByText("Cache all question results for"), @@ -548,8 +576,8 @@ describe("SaveQuestionModal", () => { setupEnterpriseTest(); }); - it("is not shown", () => { - renderSaveQuestionModal(question); + it("is not shown", async () => { + await setup(question); expect(screen.queryByText("More options")).not.toBeInTheDocument(); expect( screen.queryByText("Cache all question results for"), diff --git a/frontend/test/metabase/scenarios/models/helpers/e2e-models-metadata-helpers.js b/frontend/test/metabase/scenarios/models/helpers/e2e-models-metadata-helpers.js index 5ea638d58eb217e63370dc499b2feb8f33ffa696..6e68cb2fc7bcff1d686e13f34f06d75924d57ea0 100644 --- a/frontend/test/metabase/scenarios/models/helpers/e2e-models-metadata-helpers.js +++ b/frontend/test/metabase/scenarios/models/helpers/e2e-models-metadata-helpers.js @@ -19,7 +19,7 @@ export function mapColumnTo({ table, column } = {}) { cy.findByText("Database column this maps to") .closest(".Form-field") .findByTestId("select-button") - .click(); + .click({ force: true }); popover().contains(table).click();