Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Unit Tests for FormBuilderField and BookingFields components #16162

Merged
merged 8 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of hiding it from the normal user only(and still showing up for screen reader), hide the label completely.

This is because screen reader then sees duplicate labels. This was identified by getByRole selector in react-testing-library tests.

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} />,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify id for each element so that it can be connected to the corresponding label using htmlFor. Again, this issue was identified by react-testing-library.

},
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
Loading