Files
calendar/packages/features/form-builder/testUtils.ts
T
+1 55e6682832 feat: Custom Fields OR Add-Ons with a fee associated (#19997)
* feat: Addons to bookings

* Add tests

* Add tests

* Fix price field not shown immediately after enabling payment for an event

* Add support for all currencies in addons and fix type errors

* Address review comments

* Add tests to cover all input types

* Address cubic-dev-ai comments

* Remove openapi.json from git tracking

* Revert changes

* Revert unrelated linter/formatter changes

* Address cubic-dev-ai comments

* Addres cubic-dev-ai comment

* Update to use test id instead of hardcoded label

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Address coderabbits review comments

* Fix failing unit test

* some fixes...

* Fix tests

* Fix import

* Move handlePayment test file to appropriate location

* Fix test

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-09-16 21:33:23 +09:00

363 lines
13 KiB
TypeScript

import { fireEvent, waitFor, screen, within } from "@testing-library/react";
import { vi } from "vitest";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import type { FormBuilder } from "./FormBuilder";
export interface FieldProps {
fieldType: string;
identifier: string;
label: string;
price?: number;
options?: { label: string; value: string; price?: number }[];
}
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 getLocationBookingField = () => {
const bookingFields = getBookingFieldsWithSystemFields({
bookingFields: [],
disableGuests: false,
disableBookingTitle: false,
customInputs: [],
metadata: {},
workflows: [],
});
const locationBookingField = bookingFields.find((field) => field.name === "location");
if (!locationBookingField) {
throw new Error("location booking field not found");
}
return locationBookingField;
};
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 }) => {
const checkbox = dialog.getByTestId("field-required") as HTMLInputElement;
return checkbox.checked;
},
makeFieldOptional: ({ dialog }: { dialog: TestingLibraryElement }) => {
const checkbox = dialog.getByTestId("field-required") as HTMLInputElement;
if (checkbox.checked) {
fireEvent.click(checkbox);
}
},
makeFieldRequired: ({ dialog }: { dialog: TestingLibraryElement }) => {
const checkbox = dialog.getByTestId("field-required") as HTMLInputElement;
if (!checkbox.checked) {
fireEvent.click(checkbox);
}
},
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,
fieldType,
}: {
dialog: TestingLibraryElement;
label: string;
fieldType: string;
}) => {
if (fieldType !== "boolean") {
fireEvent.change(dialog.getAllByRole("textbox")[1], { target: { value: label } });
}
},
fillInPrice: ({ dialog, price }: { dialog: TestingLibraryElement; price: number }) => {
// The price input is rendered with an Input component with a $ suffix
const priceInput = dialog.getByRole("spinbutton");
fireEvent.change(priceInput, { target: { value: price.toString() } });
},
addOption: ({ dialog }: { dialog: TestingLibraryElement }) => {
fireEvent.click(dialog.getByTestId("add-option"));
},
fillInOption: async ({
dialog,
index,
label,
price,
}: {
dialog: TestingLibraryElement;
index: number;
label: string;
price?: number;
}) => {
pageObject.dialog.addOption({ dialog });
const optionInputs = dialog.getByTestId("options-container");
await waitFor(() => {
const listItems = optionInputs.querySelectorAll("li");
expect(listItems.length).toBeGreaterThan(index);
const optionInput = within(listItems[index]).getByRole("textbox");
expect(optionInput).toBeInTheDocument();
fireEvent.change(optionInput, { target: { value: label } });
});
if (price !== undefined) {
await waitFor(() => {
const listItems = optionInputs.querySelectorAll("li");
expect(listItems.length).toBeGreaterThan(index);
const priceInput = within(listItems[index]).getByRole("spinbutton");
expect(priceInput).toBeInTheDocument();
fireEvent.change(priceInput, { target: { value: price.toString() } });
});
}
},
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, fieldType: props.fieldType });
if (props.price !== undefined) {
pageObject.dialog.fillInPrice({ dialog, price: props.price });
}
if (props.options) {
for (let index = 0; index < props.options.length; index++) {
const option = props.options[index];
await pageObject.dialog.fillInOption({
dialog,
index,
label: option.label,
price: option.price,
});
}
}
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 }) => {
expectScenario.toHaveRequiredBadge({ identifier });
const editDialogForm = pageObject.openEditFieldDialog({ identifier });
pageObject.dialog.makeFieldOptional({ dialog: editDialogForm });
pageObject.dialog.saveField({ dialog: editDialogForm });
await waitFor(() => {
expect(screen.getByTestId("optional")).toBeInTheDocument();
});
},
verifyOptionPrices: ({ identifier, prices }: { identifier: string; prices: number[] }) => {
const dialog = pageObject.openEditFieldDialog({ identifier });
const optionsContainer = dialog.getByTestId("options-container");
const optionPriceInputs = within(optionsContainer).getAllByRole("spinbutton");
expect(optionPriceInputs).toHaveLength(prices.length);
prices.forEach((price, index) => {
expect(optionPriceInputs[index]).toHaveValue(price);
});
},
};
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 checkbox = dialog.getByTestId("field-required") as HTMLInputElement;
expect(checkbox.disabled).toBe(true);
},
toHaveLabelChangeAllowed: ({ dialog }: { dialog: TestingLibraryElement }) => {
const labelInput = dialog.getByLabelText("label");
expect(labelInput.disabled).toBe(false);
},
toNotHaveDeleteButton: ({ identifier }: { identifier: string }) => {
expect(pageObject.queryDeleteButton({ identifier })).toBeNull();
},
toNotHaveToggleButton: ({ identifier }: { identifier: string }) => {
expect(pageObject.queryToggleButton({ identifier })).toBeNull();
},
toHaveSourceBadge: ({ identifier, sourceLabel }: { identifier: string; sourceLabel: string }) => {
const field = getFieldInTheList({ identifier });
expect(field.getByText(sourceLabel)).not.toBeNull();
},
toNotHaveOptionalBadge: ({ identifier }: { identifier: string }) => {
const field = getFieldInTheList({ identifier });
expect(field.queryByTestId("optional")).toBeNull();
},
toHaveRequiredBadge: ({ identifier }: { identifier: string }) => {
const field = getFieldInTheList({ identifier });
expect(field.getByText("required")).not.toBeNull();
},
toHavePriceField: async ({ identifier, price }: { identifier: string; price: number }) => {
const dialog = pageObject.openEditFieldDialog({ identifier });
await waitFor(() => {
// Find the spinbutton input with $ suffix
const priceInput = dialog.getByRole("spinbutton");
expect(priceInput).toBeInTheDocument();
// Verify the price value is set
expect(priceInput).toHaveValue(price);
});
},
};