fix: More Stability for FormBuilder by unit tests (#16129)
* Remove use of location from FormBuilder * Add tests * Improve error message in test * Bust cache
This commit is contained in:
@@ -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<typeof fieldSchema>;
|
||||
|
||||
const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
@@ -163,7 +166,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
.map((secondaryEmail) => ({ label: secondaryEmail.email, value: secondaryEmail.id })),
|
||||
];
|
||||
const selectedSecondaryEmailId = formMethods.getValues("secondaryEmailId") || -1;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
{/**
|
||||
@@ -269,9 +271,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{...shouldLockDisableProps("bookingFields")}
|
||||
dataStore={{
|
||||
options: {
|
||||
locations: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
|
||||
locations: {
|
||||
// FormBuilder doesn't handle plural for non-english languages. So, use english(Location) only. This is similar to 'Workflow'
|
||||
source: { label: "Location" },
|
||||
value: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
|
||||
},
|
||||
},
|
||||
}}
|
||||
shouldConsiderRequired={(field: BookingField) => {
|
||||
// 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;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<RequiresConfirmationController
|
||||
|
||||
@@ -921,7 +921,8 @@ async function confirmPendingPayment(page: Page) {
|
||||
headers: { "stripe-signature": signature },
|
||||
});
|
||||
|
||||
if (response.status() !== 200) throw new Error(`Failed to confirm payment. Response: ${response.text()}`);
|
||||
if (response.status() !== 200)
|
||||
throw new Error(`Failed to confirm payment. Response: ${await response.text()}`);
|
||||
}
|
||||
|
||||
// login using a replay of an E2E routine.
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { render } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { React } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { FormBuilder } from "./FormBuilder";
|
||||
import {
|
||||
mockProps,
|
||||
verifier,
|
||||
setMockIntersectionObserver,
|
||||
setMockMatchMedia,
|
||||
pageObject,
|
||||
expectScenario,
|
||||
} from "./testUtils";
|
||||
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
const renderComponent = ({
|
||||
formBuilderProps: formBuilderProps,
|
||||
formDefaultValues: formDefaultValues,
|
||||
}: {
|
||||
formBuilderProps: Parameters<typeof FormBuilder>[0];
|
||||
formDefaultValues;
|
||||
}) => {
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => {
|
||||
const form = useForm({
|
||||
defaultValues: formDefaultValues,
|
||||
});
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<FormProvider {...form}>{children}</FormProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return render(<FormBuilder {...formBuilderProps} />, { 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,13 +45,6 @@ function getCurrentFieldType(fieldForm: UseFormReturn<RhfFormField>) {
|
||||
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<string, { label: string; value: string; inputPlaceholder?: string }[]>;
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -418,10 +425,12 @@ function FieldEditDialog({
|
||||
dialog,
|
||||
onOpenChange,
|
||||
handleSubmit,
|
||||
shouldConsiderRequired,
|
||||
}: {
|
||||
dialog: { isOpen: boolean; fieldIndex: number; data: RhfFormField | null };
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
handleSubmit: SubmitHandler<RhfFormField>;
|
||||
shouldConsiderRequired?: (field: RhfFormField) => boolean | undefined;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const fieldForm = useForm<RhfFormField>({
|
||||
@@ -531,8 +540,10 @@ function FieldEditDialog({
|
||||
<Controller
|
||||
name="required"
|
||||
control={fieldForm.control}
|
||||
render={({ field: { onChange } }) => {
|
||||
const isRequired = isRequiredField(fieldForm.getValues());
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const isRequired = shouldConsiderRequired
|
||||
? shouldConsiderRequired(fieldForm.getValues())
|
||||
: value;
|
||||
return (
|
||||
<BooleanToggleGroupField
|
||||
data-testid="field-required"
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import { fireEvent, waitFor, screen, within } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import type { FormBuilder } from "./FormBuilder";
|
||||
|
||||
export interface FieldProps {
|
||||
fieldType: string;
|
||||
identifier: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type FormBuilderProps = React.ComponentProps<typeof FormBuilder>;
|
||||
|
||||
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<typeof within>;
|
||||
|
||||
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<typeof within> }) => {
|
||||
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);
|
||||
},
|
||||
};
|
||||
@@ -58,6 +58,8 @@ model Host {
|
||||
@@index([eventTypeId])
|
||||
}
|
||||
|
||||
|
||||
|
||||
model EventType {
|
||||
id Int @id @default(autoincrement())
|
||||
/// @zod.min(1)
|
||||
|
||||
@@ -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 (
|
||||
<TooltipProvider>
|
||||
<FormProvider {...methods}>{children}</FormProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return render(<FormBuilder {...mockProps} />, { 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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<string, { label: string; value: string; inputPlaceholder?: string }[]>;
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user