From 28c631e10851e5f80b86a4b5a2407c036fd58630 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 3 Sep 2024 18:28:34 +0530 Subject: [PATCH] test: Unit Tests for FormBuilderField and BookingFields components (#16162) * Remove use of location from FormBuilder * Add tests * FormBuilderField and BookingFields tests * More tests * Remove always true if condition * Fix ui import mockig that got broken after the last merge --- .../react-awesome-query-builder/widgets.tsx | 2 +- .../BookEventForm/BookingFields.test.tsx | 183 +++++++++++++++++ .../BookEventForm/BookingFields.tsx | 5 +- .../features/bookings/lib/getBookingFields.ts | 2 +- packages/features/form-builder/Components.tsx | 14 +- .../form-builder/FormBuilder.test.tsx | 41 ++-- .../form-builder/FormBuilderField.test.tsx | 84 ++++++++ .../form-builder/FormBuilderField.tsx | 16 +- .../lib/formbuilder/test/FormBuilder.test.tsx | 77 -------- .../lib/formbuilder/test/testUtils.ts | 184 ------------------ packages/features/form-builder/testUtils.ts | 39 +++- packages/ui/__mocks__/ui.tsx | 13 ++ .../ui/components/form/inputs/TextField.tsx | 3 +- packages/ui/components/form/inputs/types.d.ts | 1 + packages/ui/index.tsx | 3 + packages/ui/package.json | 3 +- vitest.workspace.ts | 13 +- 17 files changed, 365 insertions(+), 318 deletions(-) create mode 100644 packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx create mode 100644 packages/features/form-builder/FormBuilderField.test.tsx delete mode 100644 packages/features/form-builder/lib/formbuilder/test/FormBuilder.test.tsx delete mode 100644 packages/features/form-builder/lib/formbuilder/test/testUtils.ts create mode 100644 packages/ui/__mocks__/ui.tsx diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx b/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx index 0d7220b3b23c74..d0f641edf4260a 100644 --- a/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx +++ b/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx @@ -118,7 +118,7 @@ const TextWidget = (props: TextLikeComponentPropsRAQB) => { containerClassName="w-full" type={type} value={textValue} - labelSrOnly={noLabel} + noLabel={noLabel} placeholder={placeholder} disabled={readOnly} onChange={onChange} diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx new file mode 100644 index 00000000000000..9f6909820f7b71 --- /dev/null +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx @@ -0,0 +1,183 @@ +import "@calcom/ui/__mocks__/ui"; + +import { TooltipProvider } from "@radix-ui/react-tooltip"; +import { render, fireEvent, screen } from "@testing-library/react"; +import * as React from "react"; +import type { UseFormReturn } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; +import { expect } from "vitest"; + +import { getBookingFieldsWithSystemFields } from "../../../lib/getBookingFields"; +import { BookingFields } from "./BookingFields"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FormMethods = UseFormReturn; + +const renderComponent = ({ + props: props, + formDefaultValues, +}: { + props: Parameters[0]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + formDefaultValues?: any; +}) => { + let formMethods: UseFormReturn | undefined; + const Wrapper = ({ children }: { children: React.ReactNode }) => { + const form = useForm({ + defaultValues: formDefaultValues, + }); + formMethods = form; + return ( + + {children} + + ); + }; + const result = render(, { wrapper: Wrapper }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { result, formMethods: formMethods! }; +}; + +describe("BookingFields", () => { + it("should correctly render with location fields", () => { + const AttendeePhoneNumberOption = { + label: "attendee_phone_number", + value: "phone", + }; + + const OrganizerLinkOption = { + label: "https://google.com", + value: "link", + }; + + const locations = [ + { + type: AttendeePhoneNumberOption.value, + }, + { + link: "https://google.com", + type: OrganizerLinkOption.value, + displayLocationPublicly: true, + }, + ]; + const { formMethods } = renderComponent({ + props: { + fields: getBookingFieldsWithSystemFields({ + disableGuests: false, + bookingFields: [], + metadata: null, + workflows: [], + customInputs: [], + }), + locations, + isDynamicGroupBooking: false, + bookingData: null, + }, + formDefaultValues: {}, + }); + + component.fillName({ value: "John Doe" }); + component.fillEmail({ value: "john.doe@example.com" }); + component.fillNotes({ value: "This is a note" }); + expectScenarios.expectNameToBe({ value: "John Doe", formMethods }); + expectScenarios.expectEmailToBe({ value: "john.doe@example.com", formMethods }); + expectScenarios.expectNotesToBe({ value: "This is a note", formMethods }); + + component.fillRadioInputLocation({ label: AttendeePhoneNumberOption.label, inputValue: "+1234567890" }); + expectScenarios.expectLocationToBe({ + formMethods, + label: AttendeePhoneNumberOption.label, + toMatch: { + formattedValue: "+1 (234) 567-890", + value: { optionValue: "+1234567890", value: AttendeePhoneNumberOption.value }, + }, + }); + + component.fillRadioInputLocation({ label: OrganizerLinkOption.label }); + expectScenarios.expectLocationToBe({ + formMethods, + label: OrganizerLinkOption.label, + toMatch: { + formattedValue: "+1 (234) 567-890", + value: { optionValue: "", value: OrganizerLinkOption.value }, + }, + }); + }); +}); + +const component = { + getName: ({ label = "your_name" }: { label?: string } = {}) => + screen.getByRole("textbox", { + name: new RegExp(label), + }) as HTMLInputElement, + getEmail: () => screen.getByRole("textbox", { name: /email/i }) as HTMLInputElement, + getLocationRadioOption: ({ label }: { label: string }) => + screen.getByRole("radio", { name: new RegExp(label) }) as HTMLInputElement, + getLocationRadioInput: ({ placeholder }: { placeholder: string }) => + screen.getByPlaceholderText(placeholder) as HTMLInputElement, + getNotes: () => screen.getByRole("textbox", { name: /additional_notes/i }) as HTMLInputElement, + getGuests: () => screen.getByLabelText("guests"), + fillName: ({ value }: { value: string }) => { + fireEvent.change(component.getName(), { target: { value } }); + }, + fillEmail: ({ value }: { value: string }) => { + fireEvent.change(component.getEmail(), { target: { value } }); + }, + fillRadioInputLocation: ({ label, inputValue }: { label: string; inputValue?: string }) => { + fireEvent.click(component.getLocationRadioOption({ label })); + + if (inputValue) { + let placeholder = label; + if (label === "attendee_phone_number") { + placeholder = "enter_phone_number"; + } else { + // radioInput doesn't have a label, so we need to identify by placeholder + throw new Error("Tell me how to identify the placeholder for this location input"); + } + fireEvent.change(component.getLocationRadioInput({ placeholder }), { + target: { value: inputValue }, + }); + } + }, + fillNotes: ({ value }: { value: string }) => { + fireEvent.change(component.getNotes(), { target: { value } }); + }, +}; + +const expectScenarios = { + expectNameToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { + expect(component.getName().value).toEqual(value); + expect(formMethods.getValues("responses.name")).toEqual(value); + }, + expectEmailToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { + expect(component.getEmail().value).toEqual(value); + expect(formMethods.getValues("responses.email")).toEqual(value); + }, + expectLocationToBe: ({ + formMethods, + label, + toMatch: { formattedValue, value }, + }: { + label: string; + toMatch: { + formattedValue?: string; + value: { + optionValue: string; + value: string; + }; + }; + formMethods: FormMethods; + }) => { + expect(component.getLocationRadioOption({ label }).checked).toBe(true); + if (value.optionValue) { + expect(component.getLocationRadioInput({ placeholder: "enter_phone_number" }).value).toEqual( + formattedValue + ); + } + expect(formMethods.getValues("responses.location")).toEqual(value); + }, + expectNotesToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { + expect(component.getNotes().value).toEqual(value); + expect(formMethods.getValues("responses.notes")).toEqual(value); + }, +}; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx index 8be7c1bf18a565..6c396acb21cd83 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx @@ -83,11 +83,8 @@ export const BookingFields = ({ return null; } - // Attendee location field can be edited during reschedule if (field.name === SystemField.Enum.location) { - if (locationResponse?.value === "attendeeInPerson" || "phone") { - readOnly = false; - } + readOnly = false; } // Dynamically populate location field options diff --git a/packages/features/bookings/lib/getBookingFields.ts b/packages/features/bookings/lib/getBookingFields.ts index 6990d18c9b758b..05ba5b8dcdbfce 100644 --- a/packages/features/bookings/lib/getBookingFields.ts +++ b/packages/features/bookings/lib/getBookingFields.ts @@ -17,7 +17,7 @@ import { type Fields = z.infer; -if (typeof window !== "undefined") { +if (typeof window !== "undefined" && !process.env.INTEGRATION_TEST_MODE) { // This file imports some costly dependencies, so we want to make sure it's not imported on the client side. throw new Error("`getBookingFields` must not be imported on the client side."); } diff --git a/packages/features/form-builder/Components.tsx b/packages/features/form-builder/Components.tsx index a16e6ae9a105bc..65221385704daf 100644 --- a/packages/features/form-builder/Components.tsx +++ b/packages/features/form-builder/Components.tsx @@ -97,16 +97,16 @@ type Component = export const Components: Record = { text: { propsType: propsTypes.text, - factory: (props) => , + factory: (props) => , }, textarea: { propsType: propsTypes.textarea, // TODO: Make rows configurable in the form builder - factory: (props) => , + factory: (props) => , }, number: { propsType: propsTypes.number, - factory: (props) => , + factory: (props) => , }, name: { propsType: propsTypes.name, @@ -211,7 +211,7 @@ export const Components: Record = { if (!props) { return
; } - return ; + return ; }, }, address: { @@ -219,6 +219,7 @@ export const Components: Record = { factory: (props) => { return ( { props.setValue(val); }} @@ -248,6 +249,7 @@ export const Components: Record = { {value.map((field, index) => (
  • = { ...props, listValues: props.options.map((o) => ({ title: o.label, value: o.value })), }; - return ; + return ; }, }, select: { @@ -329,7 +331,7 @@ export const Components: Record = { ...props, listValues: props.options.map((o) => ({ title: o.label, value: o.value })), }; - return ; + return ; }, }, checkbox: { diff --git a/packages/features/form-builder/FormBuilder.test.tsx b/packages/features/form-builder/FormBuilder.test.tsx index 9aaebad735cab7..0a7aeb5319e902 100644 --- a/packages/features/form-builder/FormBuilder.test.tsx +++ b/packages/features/form-builder/FormBuilder.test.tsx @@ -1,7 +1,7 @@ import { TooltipProvider } from "@radix-ui/react-tooltip"; import { render } from "@testing-library/react"; import type { ReactNode } from "react"; -import { React } from "react"; +import * as React from "react"; import { FormProvider, useForm } from "react-hook-form"; import { vi } from "vitest"; @@ -13,6 +13,7 @@ import { setMockMatchMedia, pageObject, expectScenario, + getLocationBookingField, } from "./testUtils"; vi.mock("@formkit/auto-animate/react", () => ({ @@ -115,43 +116,23 @@ describe("FormBuilder", () => { }, }, }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - shouldConsiderRequired: ({ field: any }) => { - field.name === "location" ? true : false; + // @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + shouldConsiderRequired: (field: any) => { + return 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: "", - }, - }, - }, - ], + fields: [getLocationBookingField()], }, }); // editable:'system' field can't be deleted - expect(pageObject.queryDeleteButton({ identifier: field.identifier })).toBeNull(); + expectScenario.toNotHaveDeleteButton({ identifier: field.identifier }); // editable:'system' field can't be toggled - expect(pageObject.queryToggleButton({ identifier: field.identifier })).toBeNull(); + expectScenario.toNotHaveToggleButton({ identifier: field.identifier }); + expectScenario.toHaveSourceBadge({ identifier: field.identifier, sourceLabel: "1 Location" }); + expectScenario.toHaveRequiredBadge({ identifier: field.identifier }); const newFieldDialog = pageObject.openAddFieldDialog(); diff --git a/packages/features/form-builder/FormBuilderField.test.tsx b/packages/features/form-builder/FormBuilderField.test.tsx new file mode 100644 index 00000000000000..ea804eb0e85ffb --- /dev/null +++ b/packages/features/form-builder/FormBuilderField.test.tsx @@ -0,0 +1,84 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import * as React from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import type { UseFormReturn } from "react-hook-form"; +import { vi } from "vitest"; + +import { FormBuilderField } from "./FormBuilderField"; + +vi.mock("@formkit/auto-animate/react", () => ({ + useAutoAnimate: () => [null], +})); + +const renderComponent = ({ + props: props, + formDefaultValues, +}: { + props: Parameters[0]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + formDefaultValues?: any; +}) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let formMethods: UseFormReturn | undefined; + const Wrapper = ({ children }: { children: React.ReactNode }) => { + const form = useForm({ + defaultValues: formDefaultValues, + }); + formMethods = form; + return {children}; + }; + render(, { wrapper: Wrapper }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { formMethods: formMethods! }; +}; + +describe("FormBuilderField", () => { + it("verify a text type input field", () => { + const { formMethods } = renderComponent({ + props: { + field: { + name: "textInput1", + type: "text", + label: "Text Input 1", + placeholder: "Enter text", + }, + readOnly: false, + className: "", + }, + formDefaultValues: {}, + }); + expect(component.getFieldInput({ label: "Text Input 1" }).value).toEqual(""); + component.fillFieldInput({ label: "Text Input 1", value: "test" }); + expectScenario.toHaveFieldValue({ + label: "Text Input 1", + identifier: "textInput1", + value: "test", + formMethods, + }); + }); +}); + +const component = { + getFieldInput: ({ label }) => screen.getByRole("textbox", { name: label }) as HTMLInputElement, + fillFieldInput: ({ label, value }: { label: string; value: string }) => { + fireEvent.change(component.getFieldInput({ label }), { target: { value } }); + }, +}; + +const expectScenario = { + toHaveFieldValue: ({ + identifier, + label, + value, + formMethods, + }: { + identifier: string; + label: string; + value: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + formMethods: UseFormReturn; + }) => { + expect(component.getFieldInput({ label }).value).toEqual(value); + expect(formMethods.getValues(`responses.${identifier}`)).toEqual(value); + }, +}; diff --git a/packages/features/form-builder/FormBuilderField.tsx b/packages/features/form-builder/FormBuilderField.tsx index e5cb5ee0f4ddc5..2c9c01cb1324d0 100644 --- a/packages/features/form-builder/FormBuilderField.tsx +++ b/packages/features/form-builder/FormBuilderField.tsx @@ -130,12 +130,14 @@ const WithLabel = ({ field, children, readOnly, + htmlFor, noLabel = false, }: { field: Partial; readOnly: boolean; children: React.ReactNode; noLabel?: boolean; + htmlFor: string; }) => { const { t } = useLocale(); @@ -150,7 +152,7 @@ const WithLabel = ({ field.type !== "multiemail" && field.label && (
    -