Skip to content

Commit

Permalink
test: Unit Tests for FormBuilderField and BookingFields components (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
hariombalhara authored Sep 3, 2024
1 parent 263a96e commit 28c631e
Show file tree
Hide file tree
Showing 17 changed files with 365 additions and 318 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<any>;

const renderComponent = ({
props: props,
formDefaultValues,
}: {
props: Parameters<typeof BookingFields>[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 (
<TooltipProvider>
<FormProvider {...form}>{children}</FormProvider>
</TooltipProvider>
);
};
const result = render(<BookingFields {...props} />, { 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);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/features/bookings/lib/getBookingFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {

type Fields = z.infer<typeof eventTypeBookingFields>;

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.");
}
Expand Down
14 changes: 8 additions & 6 deletions packages/features/form-builder/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,16 @@ type Component =
export const Components: Record<FieldType, Component> = {
text: {
propsType: propsTypes.text,
factory: (props) => <Widgets.TextWidget noLabel={true} {...props} />,
factory: (props) => <Widgets.TextWidget id={props.name} noLabel={true} {...props} />,
},
textarea: {
propsType: propsTypes.textarea,
// TODO: Make rows configurable in the form builder
factory: (props) => <Widgets.TextAreaWidget rows={3} {...props} />,
factory: (props) => <Widgets.TextAreaWidget id={props.name} rows={3} {...props} />,
},
number: {
propsType: propsTypes.number,
factory: (props) => <Widgets.NumberWidget noLabel={true} {...props} />,
factory: (props) => <Widgets.NumberWidget id={props.name} noLabel={true} {...props} />,
},
name: {
propsType: propsTypes.name,
Expand Down Expand Up @@ -211,14 +211,15 @@ export const Components: Record<FieldType, Component> = {
if (!props) {
return <div />;
}
return <Widgets.TextWidget type="email" noLabel={true} {...props} />;
return <Widgets.TextWidget type="email" id={props.name} noLabel={true} {...props} />;
},
},
address: {
propsType: propsTypes.address,
factory: (props) => {
return (
<AddressInput
id={props.name}
onChange={(val) => {
props.setValue(val);
}}
Expand Down Expand Up @@ -248,6 +249,7 @@ export const Components: Record<FieldType, Component> = {
{value.map((field, index) => (
<li key={index}>
<EmailField
id={`${props.name}.${index}`}
disabled={readOnly}
value={value[index]}
className={inputClassName}
Expand Down Expand Up @@ -319,7 +321,7 @@ export const Components: Record<FieldType, Component> = {
...props,
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
};
return <Widgets.MultiSelectWidget {...newProps} />;
return <Widgets.MultiSelectWidget id={props.name} {...newProps} />;
},
},
select: {
Expand All @@ -329,7 +331,7 @@ export const Components: Record<FieldType, Component> = {
...props,
listValues: props.options.map((o) => ({ title: o.label, value: o.value })),
};
return <Widgets.SelectWidget {...newProps} />;
return <Widgets.SelectWidget id={props.name} {...newProps} />;
},
},
checkbox: {
Expand Down
41 changes: 11 additions & 30 deletions packages/features/form-builder/FormBuilder.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -13,6 +13,7 @@ import {
setMockMatchMedia,
pageObject,
expectScenario,
getLocationBookingField,
} from "./testUtils";

vi.mock("@formkit/auto-animate/react", () => ({
Expand Down Expand Up @@ -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();

Expand Down
Loading

0 comments on commit 28c631e

Please sign in to comment.