diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index a35486141b..f66606129d 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -15,6 +15,7 @@ import { } from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; import type { FormValues } from "@calcom/features/eventtypes/lib/types"; import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; +import type { fieldSchema } from "@calcom/features/form-builder/schema"; import type { EditableSchema } from "@calcom/features/form-builder/schema"; import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector"; import { classNames } from "@calcom/lib"; @@ -46,6 +47,8 @@ import { import RequiresConfirmationController from "./RequiresConfirmationController"; import { DisableAllEmailsSetting } from "./settings/DisableAllEmailsSetting"; +type BookingField = z.infer; + const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal")); export const EventAdvancedTab = ({ eventType, team }: Pick) => { @@ -163,7 +166,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick ({ label: secondaryEmail.email, value: secondaryEmail.id })), ]; const selectedSecondaryEmailId = formMethods.getValues("secondaryEmailId") || -1; - return (
{/** @@ -269,9 +271,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { + // Location field has a default value at backend so API can send no location but we don't allow it in UI and thus we want to show it as required to user + return field.name === "location" ? true : field.required; + }} />
({ + useAutoAnimate: () => [null], +})); + +const renderComponent = ({ + formBuilderProps: formBuilderProps, + formDefaultValues: formDefaultValues, +}: { + formBuilderProps: Parameters[0]; + formDefaultValues; +}) => { + const Wrapper = ({ children }: { children: ReactNode }) => { + const form = useForm({ + defaultValues: formDefaultValues, + }); + return ( + + {children} + + ); + }; + + return render(, { wrapper: Wrapper }); +}; + +describe("FormBuilder", () => { + beforeAll(() => { + setMockMatchMedia(); + setMockIntersectionObserver(); + }); + + describe("Basic Tests", () => { + const fieldTypes = [ + { fieldType: "email", label: "Email Field" }, + { fieldType: "phone", label: "Phone Field" }, + { fieldType: "address", label: "Address Field" }, + { fieldType: "text", label: "Short Text Field" }, + { fieldType: "number", label: "Number Field" }, + { fieldType: "textarea", label: "LongText Field" }, + { fieldType: "select", label: "Select Field" }, + { fieldType: "multiselect", label: "MultiSelect Field" }, + { fieldType: "multiemail", label: "Multiple Emails Field" }, + { fieldType: "checkbox", label: "CheckBox Group Field" }, + { fieldType: "radio", label: "Radio Group Field" }, + { fieldType: "boolean", label: "Checkbox Field" }, + ]; + beforeEach(() => { + renderComponent({ formBuilderProps: mockProps, formDefaultValues: {} }); + }); + + for (const { fieldType, label } of fieldTypes) { + it(`Should add new field of type ${fieldType} `, async () => { + const defaultIdentifier = `${fieldType}-id`; + const newIdentifier = `${defaultIdentifier}-edited`; + + await verifier.verifyFieldAddition({ + fieldType, + identifier: defaultIdentifier, + label, + }); + + await verifier.verifyIdentifierChange({ + newIdentifier: newIdentifier, + existingIdentifier: defaultIdentifier, + }); + + await verifier.verifyThatFieldCanBeMarkedOptional({ + identifier: newIdentifier, + }); + + await verifier.verifyFieldToggle({ identifier: newIdentifier }); + + await verifier.verifyFieldDeletion({ identifier: newIdentifier }); + }); + } + }); + + describe("radioInput type field with options available in dataStore", () => { + test('Should add new field of type "radioInput" and select option from dataStore', async () => { + const field = { + identifier: "location", + type: "radioInput", + }; + renderComponent({ + formBuilderProps: { + ...mockProps, + dataStore: { + options: { + locations: { + value: [ + { + label: "Attendee Phone", + value: "phone", + inputPlaceholder: "Phone Number", + }, + ], + source: { label: "Location" }, + }, + }, + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + shouldConsiderRequired: ({ field: any }) => { + field.name === "location" ? true : false; + }, + }, + // TODO: May be we should get this from getBookingFields directly which tests more practical cases + formDefaultValues: { + fields: [ + { + defaultLabel: "location", + type: field.type, + name: field.identifier, + editable: "system", + hideWhenJustOneOption: true, + required: false, + getOptionsAt: "locations", + optionsInputs: { + attendeeInPerson: { + type: "address", + required: true, + placeholder: "", + }, + phone: { + type: "phone", + required: true, + placeholder: "", + }, + }, + }, + ], + }, + }); + + // editable:'system' field can't be deleted + expect(pageObject.queryDeleteButton({ identifier: field.identifier })).toBeNull(); + // editable:'system' field can't be toggled + expect(pageObject.queryToggleButton({ identifier: field.identifier })).toBeNull(); + + const newFieldDialog = pageObject.openAddFieldDialog(); + + // radioInput type field isn't selectable by the user. + expect( + pageObject.dialog.fieldTypeDropdown.queryOptionForFieldType({ + dialog: newFieldDialog, + fieldType: "radioInput", + }) + ).toBeNull(); + + pageObject.dialog.close({ dialog: newFieldDialog }); + + const dialog = pageObject.openEditFieldDialog({ identifier: field.identifier }); + + expectScenario.toHaveFieldTypeDropdownDisabled({ dialog }); + expectScenario.toHaveIdentifierChangeDisabled({ dialog }); + expectScenario.toHaveRequirablityToggleDisabled({ dialog }); + expectScenario.toHaveLabelChangeAllowed({ dialog }); + expect(pageObject.dialog.isFieldShowingAsRequired({ dialog })).toBe(true); + }); + }); +}); diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx index ebb0b8de3d..88ee25f90f 100644 --- a/packages/features/form-builder/FormBuilder.tsx +++ b/packages/features/form-builder/FormBuilder.tsx @@ -45,13 +45,6 @@ function getCurrentFieldType(fieldForm: UseFormReturn) { return fieldTypesConfigMap[fieldForm.watch("type") || "text"]; } -function isRequiredField(field: RhfFormField) { - // 'location' must be shown as required in the UI(but at backend it is not required because not specifying it defaults to CalVideo) - // So, handle it in UI only instead of updating bookingFields config in getBookingFields - // TODO: FormBuilder must not be made aware of BookingFields, so move this "location" check to outside FormBuilder where the component is used. - return field.name === "location" ? true : field.required; -} - /** * It works with a react-hook-form only. * `formProp` specifies the name of the property in the react-hook-form that has the fields. This is where fields would be updated. @@ -64,6 +57,7 @@ export const FormBuilder = function FormBuilder({ disabled, LockedIcon, dataStore, + shouldConsiderRequired, }: { formProp: string; title: string; @@ -75,8 +69,19 @@ export const FormBuilder = function FormBuilder({ * A readonly dataStore that is used to lookup the options for the fields. It works in conjunction with the field.getOptionAt property which acts as the key in options */ dataStore: { - options: Record; + options: Record< + string, + { + source: { label: string }; + value: { label: string; value: string; inputPlaceholder?: string }[]; + } + >; }; + /** + * This is kind of a hack to allow certain fields to be just shown as required when they might not be required in a strict sense + * e.g. Location field has a default value at backend so API can send no location but formBuilder in UI doesn't allow it. + */ + shouldConsiderRequired?: (field: RhfFormField) => boolean | undefined; }) { // I would have liked to give Form Builder it's own Form but nested Forms aren't something that browsers support. // So, this would reuse the same Form as the parent form. @@ -128,12 +133,13 @@ export const FormBuilder = function FormBuilder({ {fields.map((field, index) => { let options = field.options ?? null; const sources = [...(field.sources || [])]; - const isRequired = isRequiredField(field); + const isRequired = shouldConsiderRequired ? shouldConsiderRequired(field) : field.required; if (!options && field.getOptionsAt) { - options = dataStore.options[field.getOptionsAt as keyof typeof dataStore] ?? []; - // TODO: The dataStore should itself provide the label directly - // This is important because FormBuilder isn't aware of what a location is. It is a generic Form Builder - const sourceLabel = field.getOptionsAt === "locations" ? "Location" : field.getOptionsAt; + const { + source: { label: sourceLabel }, + value, + } = dataStore.options[field.getOptionsAt as keyof typeof dataStore] ?? []; + options = value; options.forEach((option) => { sources.push({ id: option.value, @@ -321,6 +327,7 @@ export const FormBuilder = function FormBuilder({ data: null, }); }} + shouldConsiderRequired={shouldConsiderRequired} /> )} @@ -418,10 +425,12 @@ function FieldEditDialog({ dialog, onOpenChange, handleSubmit, + shouldConsiderRequired, }: { dialog: { isOpen: boolean; fieldIndex: number; data: RhfFormField | null }; onOpenChange: (isOpen: boolean) => void; handleSubmit: SubmitHandler; + shouldConsiderRequired?: (field: RhfFormField) => boolean | undefined; }) { const { t } = useLocale(); const fieldForm = useForm({ @@ -531,8 +540,10 @@ function FieldEditDialog({ { - const isRequired = isRequiredField(fieldForm.getValues()); + render={({ field: { value, onChange } }) => { + const isRequired = shouldConsiderRequired + ? shouldConsiderRequired(fieldForm.getValues()) + : value; return ( ; + +export const mockProps: FormBuilderProps = { + formProp: "fields", + title: "FormBuilder Title", + description: "FormBuilder Description", + addFieldLabel: "Add Field", + disabled: false, + LockedIcon: false, + dataStore: { options: {} }, +}; + +export const setMockMatchMedia = () => { + Object.defineProperty(window, "matchMedia", { + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +}; + +export const setMockIntersectionObserver = () => { + Object.defineProperty(window, "IntersectionObserver", { + value: vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + takeRecords: vi.fn(() => []), + })), + }); +}; + +type TestingLibraryElement = ReturnType; + +const getFieldDomElementInTheList = ({ identifier }: { identifier: string }) => { + return screen.getByTestId(`field-${identifier}`); +}; + +const queryFieldDomElementInTheList = ({ identifier }: { identifier: string }) => { + return screen.queryByTestId(`field-${identifier}`); +}; + +const getFieldInTheList = ({ identifier }: { identifier: string }) => { + const domElement = getFieldDomElementInTheList({ identifier }); + return within(domElement); +}; + +const getEditDialogForm = () => { + const formBuilder = document.getElementById("form-builder"); + if (!formBuilder) { + throw new Error("Form Builder not found"); + } + return within(formBuilder); +}; + +export const pageObject = { + openAddFieldDialog: () => { + fireEvent.click(screen.getByTestId("add-field")); + return getEditDialogForm(); + }, + openEditFieldDialog: ({ identifier }: { identifier: string }) => { + fireEvent.click(getFieldInTheList({ identifier }).getByTestId("edit-field-action")); + return getEditDialogForm(); + }, + dialog: { + isFieldShowingAsRequired: ({ dialog }: { dialog: TestingLibraryElement }) => { + return ( + within(dialog.getByTestId("field-required")).getByText("Yes").getAttribute("aria-checked") === "true" + ); + }, + makeFieldOptional: ({ dialog }: { dialog: TestingLibraryElement }) => { + fireEvent.click(within(dialog.getByTestId("field-required")).getByText("No")); + }, + makeFieldRequired: ({ dialog }: { dialog: TestingLibraryElement }) => { + fireEvent.click(within(dialog.getByTestId("field-required")).getByText("Yes")); + }, + queryIdentifierInput: ({ dialog }: { dialog: TestingLibraryElement }) => { + return dialog.getByLabelText("identifier"); + }, + saveField: ({ dialog }: { dialog: ReturnType }) => { + fireEvent.click(dialog.getByTestId("field-add-save")); + }, + openFieldTypeDropdown: ({ dialog }: { dialog: TestingLibraryElement }) => { + fireEvent.keyDown(dialog.getByTestId("test-field-type"), { key: "ArrowDown", code: "ArrowDown" }); + }, + fieldTypeDropdown: { + queryOptionForFieldType: ({ + dialog, + fieldType, + }: { + dialog: TestingLibraryElement; + fieldType: string; + }) => { + const fieldTypeEl = within(dialog.getByTestId("test-field-type")); + return fieldTypeEl.queryByTestId(`select-option-${fieldType}`); + }, + queryAllOptions: ({ dialog }: { dialog: TestingLibraryElement }) => { + const fieldTypeElementWithOptions = within(dialog.getByTestId("test-field-type").parentElement); + return fieldTypeElementWithOptions.queryAllByTestId(/^select-option-/); + }, + }, + selectFieldType: ({ dialog, fieldType }: { dialog: TestingLibraryElement; fieldType: string }) => { + pageObject.dialog.openFieldTypeDropdown({ dialog }); + fireEvent.click(dialog.getByTestId(`select-option-${fieldType}`)); + }, + fillInFieldIdentifier: ({ + dialog, + identifier, + }: { + dialog: TestingLibraryElement; + identifier: string; + }) => { + fireEvent.change(dialog.getAllByRole("textbox")[0], { target: { value: identifier } }); + }, + fillInFieldLabel: ({ dialog, label }: { dialog: TestingLibraryElement; label: string }) => { + fireEvent.change(dialog.getAllByRole("textbox")[1], { target: { value: label } }); + }, + close: ({ dialog }: { dialog: TestingLibraryElement }) => { + fireEvent.click(dialog.getByTestId("dialog-rejection")); + }, + }, + queryDeleteButton: ({ identifier }: { identifier: string }) => { + const field = getFieldInTheList({ identifier }); + return field.queryByTestId("delete-field-action"); + }, + getDeleteButton: ({ identifier }: { identifier: string }) => { + const field = getFieldInTheList({ identifier }); + return field.queryByTestId("delete-field-action"); + }, + deleteField: ({ identifier }: { identifier: string }) => { + const field = getFieldInTheList({ identifier }); + fireEvent.click(field.getByTestId("delete-field-action")); + }, + queryToggleButton: ({ identifier }: { identifier: string }) => { + const field = getFieldInTheList({ identifier }); + return field.queryByTestId("toggle-field"); + }, + getToggleButton: ({ identifier }: { identifier: string }) => { + const field = getFieldInTheList({ identifier }); + return field.getByTestId("toggle-field"); + }, + toggleField: ({ identifier }: { identifier: string }) => { + const field = getFieldInTheList({ identifier }); + fireEvent.click(field.getByTestId("toggle-field")); + }, +}; + +export const verifier = { + verifyFieldAddition: async (props: FieldProps) => { + const dialog = pageObject.openAddFieldDialog(); + pageObject.dialog.selectFieldType({ dialog, fieldType: props.fieldType }); + pageObject.dialog.fillInFieldIdentifier({ dialog, identifier: props.identifier }); + pageObject.dialog.fillInFieldLabel({ dialog, label: props.label }); + pageObject.dialog.saveField({ dialog: getEditDialogForm() }); + + await waitFor(() => { + expect(getFieldDomElementInTheList({ identifier: props.identifier })).toBeInTheDocument(); + }); + }, + verifyIdentifierChange: async (props: { newIdentifier: string; existingIdentifier: string }) => { + const dialog = pageObject.openEditFieldDialog({ identifier: props.existingIdentifier }); + pageObject.dialog.fillInFieldIdentifier({ dialog, identifier: props.newIdentifier }); + pageObject.dialog.saveField({ dialog }); + + await waitFor(() => { + expect(getFieldDomElementInTheList({ identifier: props.newIdentifier })).toBeInTheDocument(); + expect(queryFieldDomElementInTheList({ identifier: props.existingIdentifier })).not.toBeInTheDocument(); + }); + }, + verifyFieldDeletion: async ({ identifier }: { identifier: string }) => { + pageObject.deleteField({ identifier }); + + await waitFor(() => { + expect(queryFieldDomElementInTheList({ identifier })).not.toBeInTheDocument(); + }); + }, + verifyFieldToggle: async ({ identifier }: { identifier: string }) => { + pageObject.toggleField({ identifier }); + + await waitFor(() => { + expect(screen.queryByText(/hidden/i)).toBeInTheDocument(); + }); + }, + verifyThatFieldCanBeMarkedOptional: async ({ identifier }: { identifier: string }) => { + const field = getFieldInTheList({ identifier }); + expect(field.getByTestId("required")).toBeInTheDocument(); + const editDialogForm = pageObject.openEditFieldDialog({ identifier }); + pageObject.dialog.makeFieldOptional({ dialog: editDialogForm }); + pageObject.dialog.saveField({ dialog: editDialogForm }); + + await waitFor(() => { + expect(screen.getByTestId("optional")).toBeInTheDocument(); + }); + }, +}; + +export const expectScenario = { + toHaveFieldTypeDropdownDisabled: ({ dialog }: { dialog: TestingLibraryElement }) => { + pageObject.dialog.openFieldTypeDropdown({ dialog }); + const allOptions = pageObject.dialog.fieldTypeDropdown.queryAllOptions({ dialog }); + if (allOptions.length > 0) { + throw new Error(`Field type dropdown should be disabled`); + } + }, + toHaveIdentifierChangeDisabled: ({ dialog }: { dialog: TestingLibraryElement }) => { + const identifierInput = pageObject.dialog.queryIdentifierInput({ dialog }); + expect(identifierInput.disabled).toBe(true); + }, + toHaveRequirablityToggleDisabled: ({ dialog }: { dialog: TestingLibraryElement }) => { + const no = within(dialog.getByTestId("field-required")).getByText("No"); + const yes = within(dialog.getByTestId("field-required")).getByText("Yes"); + expect(no.disabled).toBe(true); + expect(yes.disabled).toBe(true); + }, + toHaveLabelChangeAllowed: ({ dialog }: { dialog: TestingLibraryElement }) => { + const labelInput = dialog.getByLabelText("label"); + expect(labelInput.disabled).toBe(false); + }, +}; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 8005f83c02..d1127071b8 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -58,6 +58,8 @@ model Host { @@index([eventTypeId]) } + + model EventType { id Int @id @default(autoincrement()) /// @zod.min(1) diff --git a/packages/ui/components/all-questions/tests/allQuestions.test.tsx b/packages/ui/components/all-questions/tests/allQuestions.test.tsx deleted file mode 100644 index 5ba1fe0634..0000000000 --- a/packages/ui/components/all-questions/tests/allQuestions.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { TooltipProvider } from "@radix-ui/react-tooltip"; -import { render } from "@testing-library/react"; -import type { ReactNode } from "react"; -import { FormProvider, useForm } from "react-hook-form"; -import { vi } from "vitest"; - -import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; - -import { mockProps, questionUtils, setMockIntersectionObserver, setMockMatchMedia } from "./utils"; - -vi.mock("@formkit/auto-animate/react", () => ({ - useAutoAnimate: () => [null], -})); - -const renderComponent = () => { - const Wrapper = ({ children }: { children: ReactNode }) => { - const methods = useForm(); - return ( - - {children} - - ); - }; - - return render(, { wrapper: Wrapper }); -}; - -describe("MultiSelect Question", () => { - beforeEach(() => { - renderComponent(); - }); - - beforeAll(() => { - setMockMatchMedia(); - setMockIntersectionObserver(); - }); - - const questionTypes = [ - { questionType: "email", label: "Email Field" }, - { questionType: "phone", label: "Phone Field" }, - { questionType: "address", label: "Address Field" }, - { questionType: "text", label: "Short Text Field" }, - { questionType: "number", label: "Number Field" }, - { questionType: "textarea", label: "LongText Field" }, - { questionType: "select", label: "Select Field" }, - { questionType: "multiselect", label: "MultiSelect Field" }, - { questionType: "multiemail", label: "Multiple Emails Field" }, - { questionType: "checkbox", label: "CheckBox Group Field" }, - { questionType: "radio", label: "Radio Group Field" }, - { questionType: "boolean", label: "Checkbox Field" }, - ]; - - for (const { questionType, label } of questionTypes) { - it(`Should handle ${label}`, async () => { - const defaultIdentifier = `${questionType}-id`; - const newIdentifier = `${defaultIdentifier}-edited`; - - await questionUtils.addQuestion({ - questionType, - identifier: defaultIdentifier, - label, - }); - - await questionUtils.editQuestion({ - identifier: newIdentifier, - existingQuestionId: defaultIdentifier, - }); - - await questionUtils.requiredAndOptionalQuestion(); - - await questionUtils.hideQuestion(); - - await questionUtils.deleteQuestion(newIdentifier); - }); - } -}); diff --git a/packages/ui/components/all-questions/tests/utils.ts b/packages/ui/components/all-questions/tests/utils.ts deleted file mode 100644 index 25cd236450..0000000000 --- a/packages/ui/components/all-questions/tests/utils.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { fireEvent, waitFor, screen } from "@testing-library/react"; -import { vi } from "vitest"; - -export interface QuestionProps { - questionType: string; - identifier: string; - label: string; -} - -interface FormBuilderProps { - formProp: string; - title: string; - description: string; - addFieldLabel: string; - disabled: boolean; - LockedIcon: false | JSX.Element; - dataStore: { - options: Record; - }; -} - -export const mockProps: FormBuilderProps = { - formProp: "formProp", - title: "FormBuilder Title", - description: "FormBuilder Description", - addFieldLabel: "Add Field", - disabled: false, - LockedIcon: false, - dataStore: { options: {} }, -}; - -export const setMockMatchMedia = () => { - Object.defineProperty(window, "matchMedia", { - value: vi.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); -}; - -export const setMockIntersectionObserver = () => { - Object.defineProperty(window, "IntersectionObserver", { - value: vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), - takeRecords: vi.fn(() => []), - })), - }); -}; - -export const questionUtils = { - addQuestion: async (props: QuestionProps) => { - fireEvent.click(screen.getByTestId("add-field")); - fireEvent.keyDown(screen.getByTestId("test-field-type"), { key: "ArrowDown", code: "ArrowDown" }); - fireEvent.click(screen.getByTestId("select-option-email")); - fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: props.identifier } }); - fireEvent.change(screen.getAllByRole("textbox")[1], { target: { value: props.label } }); - fireEvent.click(screen.getByTestId("field-add-save")); - - await waitFor(() => { - expect(screen.queryByTestId(`field-${props.identifier}`)).toBeInTheDocument(); - }); - }, - editQuestion: async (props: { identifier: string; existingQuestionId: string }) => { - fireEvent.click(screen.getByTestId("edit-field-action")); - fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: props.identifier } }); - fireEvent.click(screen.getByTestId("field-add-save")); - - await waitFor(() => { - expect(screen.queryByTestId(`field-${props.identifier}`)).toBeInTheDocument(); - expect(screen.queryByTestId(`field-${props.existingQuestionId}`)).not.toBeInTheDocument(); - }); - }, - deleteQuestion: async (existingQuestionId: string) => { - expect(screen.queryByTestId(`field-${existingQuestionId}`)).toBeInTheDocument(); - - fireEvent.click(screen.getByTestId("delete-field-action")); - - await waitFor(() => { - expect(screen.queryByTestId(`field-${existingQuestionId}`)).not.toBeInTheDocument(); - }); - }, - hideQuestion: async () => { - fireEvent.click(screen.getByTestId("toggle-field")); - - await waitFor(() => { - expect(screen.queryByText(/hidden/i)).toBeInTheDocument(); - }); - }, - requiredAndOptionalQuestion: async () => { - expect(screen.queryByTestId("required")).toBeInTheDocument(); - - fireEvent.click(screen.getByTestId("edit-field-action")); - fireEvent.click(screen.getAllByRole("radio")[1]); - fireEvent.click(screen.getByTestId("field-add-save")); - - await waitFor(() => { - expect(screen.getByTestId("optional")).toBeInTheDocument(); - }); - }, -}; diff --git a/vitest.workspace.ts b/vitest.workspace.ts index ae6537e1c3..3ee1abb996 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -107,6 +107,7 @@ const workspaces = packagedEmbedTestsOnly globals: true, name: "@calcom/features", include: ["packages/features/**/*.{test,spec}.tsx"], + exclude: ["packages/features/form-builder/**/*"], environment: "jsdom", setupFiles: ["setupVitest.ts", "packages/ui/components/test-setup.ts"], }, @@ -149,6 +150,15 @@ const workspaces = packagedEmbedTestsOnly setupFiles: ["packages/ui/components/test-setup.ts"], }, }, + { + test: { + globals: true, + name: "@calcom/features/form-builder", + include: ["packages/features/form-builder/**/*.{test,spec}.[jt]s?(x)"], + environment: "jsdom", + setupFiles: ["packages/ui/components/test-setup.ts"], + }, + }, { test: { globals: true,