* fix * move event types components to web * update import paths * mv apps components * migrate form builder * fix * mv sso * fix * mv * update import paths * update import paths * mv * mv * mv * fix * update Booker * fix * fix * fix * fix * mv video * mv embed components to web * update import paths * mv calendar weekly view components * update import paths * fix * fixp * fix * fix * fix * fix: update FormBuilder imports to use @calcom/features/form-builder Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update broken import paths after file migrations Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: correct import paths for platform atoms and moved components Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: apply CSS type fixes and add missing atoms exports Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: resolve type errors in test files after component migrations Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: resolve remaining type errors in test files Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * migrate * fix: resolve type errors in test and mock files - Add missing bookingForm, bookerFormErrorRef, instantConnectCooldownMs to Booker.test.tsx bookings prop - Add all required BookerEvent properties to event.mock.ts - Add vi import from vitest to all mock files - Fix date parameter types in packages/dayjs/__mocks__/index.ts - Add verificationCode and setVerificationCode to test-utils.tsx mock store - Remove children.type access in Section.tsx mock to fix type error - Fix lint issues: remove unused React imports, use import type where needed, add return types - Add biome-ignore comments for pre-existing lint warnings in test files Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * migrate * migrate * migrate * update import paths * update import paths * update import paths * fix * migrate data table * migrate data table * fix * fix * fix * migrate insights components * migrate insights components * fix * mv * update import paths * fix * fix * fix * fix * fix * fix: resolve type errors in test mocks - Booker.test.tsx: Add all required UseFormReturn methods to bookingForm mock - event.mock.ts: Fix entity, subsetOfHosts, instantMeetingParameters, fieldTranslations, image types - dayjs/__mocks__/index.ts: Use Object.assign for proper typing of mock properties - Section.tsx: Change 'class' to 'className' in JSX with biome-ignore comment Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing hasDataErrors and dataErrors to bookings.errors mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing loadingStates properties to bookings mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing slots properties (setTentativeSelectedTimeslots, tentativeSelectedTimeslots, slotReservationId) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update quickAvailabilityChecks to include utcEndIso and use valid status type Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing bookerForm properties (formName, beforeVerifyEmail, formErrors) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing UseFormReturn properties to bookerForm.bookingForm mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add hasFormErrors and formErrors to bookerForm.formErrors mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add hasFormErrors and formErrors to bookerForm.errors mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing isError property to mockEvent Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use complete BookerEvent mock in Booker.test.tsx Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use branded bookingFields type in event mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing schedule mock properties (isError, isSuccess, isLoading, dataUpdatedAt) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * revert * fix * fix * fix * fix build * fix * fix * fix * fix: correct AddMembersWithSwitch test wrapper to use initial assignAllTeamMembers value - Fixed test wrapper to initialize useState with componentProps.assignAllTeamMembers instead of hardcoded false, allowing tests to properly test different states - Updated test expectations for ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_NOT_APPLICABLE state to match actual component behavior (toggle should be present and checked) - Fixed 'should show Segment when toggled on' test to start with assignAllTeamMembers: false to properly test the flow of enabling it - Added explicit types to satisfy biome lint requirements Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use JSX.Element instead of React.JSX.Element for type compatibility Co-Authored-By: benny@cal.com <sldisek783@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
359 lines
13 KiB
TypeScript
359 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";
|
|
|
|
export interface FieldProps {
|
|
fieldType: string;
|
|
identifier: string;
|
|
label: string;
|
|
price?: number;
|
|
options?: { label: string; value: string; price?: number }[];
|
|
}
|
|
|
|
export const mockProps = {
|
|
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);
|
|
});
|
|
},
|
|
};
|