* wip * Add preview mode in router * only fetch bookings of this month for weighted rr * test set up * only get bookings of current month * reverts test setup * first changes for weight adjustments * reset booking count + adjust calibration * depreciate weightAdjustment * get only bookings created this month * fix typo * make sure createdAt for hosts is correct * use earliest possibel date as fallback * add missing createdAt date to tests * fix typo * clean up changes in tests * fix typo * change end date to current date * fix: Fall back to empty host array when no hosts are found * fix: Restructure code a little * fixed test, incorrectly used now outdated var * perf: remove Dayjs from getLuckyUser * Refactor getHostsWithCalibration for optimised performance, intentionally break test as findMany is always an array * Better mock for host.findMany * Remove team-event-types.test.ts, move to appropriate package * TypeScript cannot auto-infer that an array is non-empty when assigning to a var * fix: Type Fixes and DistributionMethod enum add * Optimise tests * Added test to show that bookings made before a newHost was added affect the lucky user result * Throw error when the usersWithHighestPriority is empty, which should never happen * WIP * remove comment * update migrations * get attributes weights and virtual queue data * use attribute weights and use bookings of virtual queue only * clean up migrations * Add shortfall column and add tests * code clean up from feedback * wrapper function for getLuckyUser * code clean up * fetch routingFormResponse in handleNewBooking * fix type errors * fix type errors in tests * fix getAttributesQueryValue import for tests * fix totalWeight * add test for attributes weights and virtual queues * clean up code * add test for prepareQueuesAndAttributesData * remove console.log * use lazy import * Add more tests and more columns to matching members queue * fix issue from merge * always send usersAndTheirBookingShortfalls --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
501 lines
16 KiB
TypeScript
501 lines
16 KiB
TypeScript
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
import { vi } from "vitest";
|
|
|
|
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
|
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
|
|
|
import { RerouteDialog } from "../RerouteDialog";
|
|
|
|
const mockRouter = {
|
|
push: vi.fn((path: string) => {
|
|
return;
|
|
}),
|
|
};
|
|
|
|
vi.mock("next/navigation", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("next/navigation")>();
|
|
return {
|
|
...actual,
|
|
useRouter: vi.fn(() => mockRouter),
|
|
};
|
|
});
|
|
|
|
vi.mock("@calcom/app-store/routing-forms/lib/processRoute", () => ({
|
|
findMatchingRoute: vi.fn(({ form, response }) => {
|
|
return form.routes.find((route: any) => route.__testMatching);
|
|
}),
|
|
}));
|
|
|
|
const fakeNewTabWindow = {
|
|
close: vi.fn(),
|
|
};
|
|
|
|
const mockOpen = vi.fn((_url: string) => {
|
|
return fakeNewTabWindow;
|
|
});
|
|
|
|
vi.stubGlobal("open", mockOpen);
|
|
|
|
vi.mock("@calcom/app-store/routing-forms/components/FormInputFields", () => ({
|
|
default: vi.fn(({ response, form, setResponse, disabledFields }) => {
|
|
return (
|
|
<div data-testid="mock-form-input-fields">
|
|
{form.fields?.map((field: any) => (
|
|
<div key={field.id}>
|
|
<label data-testid={`mock-form-field-${field.id}-label`}>{field.label}</label>
|
|
<div data-testid={`mock-form-field-${field.id}-value`}>{response[field.id]?.value}</div>
|
|
</div>
|
|
))}
|
|
<div data-testid="mock-form-field-disabled-fields-identifiers">{disabledFields.join(", ")}</div>
|
|
<button
|
|
data-testid="mock-form-update-response-button"
|
|
onClick={() =>
|
|
setResponse({
|
|
"company-size": {
|
|
label: "Company Size",
|
|
value: "small",
|
|
},
|
|
country: {
|
|
label: "Country",
|
|
value: "usa",
|
|
},
|
|
})
|
|
}>
|
|
Test Update Response
|
|
</button>
|
|
</div>
|
|
);
|
|
}),
|
|
FormInputFieldsSkeleton: vi.fn(() => <div data-testid="mock-form-input-fields-skeleton" />),
|
|
}));
|
|
|
|
vi.mock("@calcom/lib/hooks/useLocale", () => ({
|
|
useLocale: vi.fn(() => ({ t: (key: string) => key })),
|
|
}));
|
|
|
|
vi.mock("@calcom/web/lib/hooks/useRouterQuery", () => ({
|
|
default: vi.fn(() => {
|
|
return {
|
|
setQuery: vi.fn(),
|
|
};
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@calcom/ui", async (importOriginal) => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
const actual = await importOriginal<any>("@calcom/ui");
|
|
return {
|
|
...actual,
|
|
Tooltip: vi.fn(({ children }) => <div data-testid="mock-tooltip">{children}</div>),
|
|
};
|
|
});
|
|
|
|
vi.mock("@calcom/trpc/react", () => ({
|
|
trpc: {
|
|
viewer: {
|
|
appRoutingForms: {
|
|
getResponseWithFormFields: {
|
|
useQuery: vi.fn(() => ({
|
|
data: {
|
|
form: {
|
|
id: "form-id",
|
|
name: "Test Form",
|
|
fields: [
|
|
{
|
|
id: "company-size",
|
|
label: "Company Size",
|
|
type: "select",
|
|
options: [
|
|
{ label: "Small", value: "small" },
|
|
{ label: "Medium", value: "medium" },
|
|
{ label: "Large", value: "large" },
|
|
],
|
|
},
|
|
{
|
|
id: "country",
|
|
label: "Country",
|
|
type: "select",
|
|
options: [
|
|
{ label: "USA", value: "usa" },
|
|
{ label: "Canada", value: "canada" },
|
|
{ label: "UK", value: "uk" },
|
|
],
|
|
},
|
|
{
|
|
id: "email",
|
|
label: "Email",
|
|
type: "email",
|
|
required: true,
|
|
},
|
|
],
|
|
routes: [
|
|
{
|
|
id: "mock-route-id",
|
|
action: {
|
|
eventTypeId: 123,
|
|
type: RouteActionType.EventTypeRedirectUrl,
|
|
value: "team/test-team/new-test-event",
|
|
},
|
|
__testMatching: true,
|
|
},
|
|
{
|
|
id: "fallback-route",
|
|
isFallback: true,
|
|
action: {
|
|
type: RouteActionType.CustomPageMessage,
|
|
value: "456",
|
|
},
|
|
__testMatching: false,
|
|
},
|
|
],
|
|
},
|
|
response: {
|
|
"company-size": {
|
|
label: "Company Size",
|
|
value: "small",
|
|
},
|
|
country: {
|
|
label: "Country",
|
|
value: "usa",
|
|
},
|
|
},
|
|
},
|
|
isPending: false,
|
|
})),
|
|
},
|
|
},
|
|
eventTypes: {
|
|
get: {
|
|
useQuery: vi.fn(() => ({
|
|
data: {
|
|
eventType: {
|
|
id: "123",
|
|
title: "Mocked Event Type",
|
|
slug: "mocked-event-type",
|
|
length: 30,
|
|
schedulingType: "ROUND_ROBIN",
|
|
team: { slug: "mocked-team" },
|
|
},
|
|
},
|
|
isPending: false,
|
|
})),
|
|
},
|
|
},
|
|
routingForms: {
|
|
findTeamMembersMatchingAttributeLogic: {
|
|
useMutation: vi.fn(({ onSuccess }) => {
|
|
return {
|
|
mutate: vi.fn(() => {
|
|
onSuccess({
|
|
result: {
|
|
users: [
|
|
{
|
|
id: 1,
|
|
name: "Matching User 1",
|
|
email: "matching-user-1@example.com",
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "Matching User 2",
|
|
email: "matching-user-2@example.com",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}),
|
|
};
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
const mockReactQueryMutateFn = vi.fn(({ __testOnSuccess }) => {
|
|
__testOnSuccess({
|
|
uid: "RESCHEDULED_BOOKING_UID_SAME_TIMESLOT",
|
|
});
|
|
});
|
|
|
|
vi.mock("@tanstack/react-query", () => ({
|
|
useMutation: vi.fn(({ onSuccess }) => {
|
|
return {
|
|
mutate: vi.fn((payload) => {
|
|
mockReactQueryMutateFn({
|
|
...payload,
|
|
__testOnSuccess: onSuccess,
|
|
});
|
|
}),
|
|
isLoading: false,
|
|
isError: false,
|
|
error: null,
|
|
onSuccess: onSuccess,
|
|
};
|
|
}),
|
|
}));
|
|
|
|
async function mockMessageFromOpenedTab({ type, data }: { type: string; data: any }) {
|
|
const messageReceivedPromise = new Promise<boolean>((resolve) => {
|
|
window.addEventListener("message", () => {
|
|
resolve(true);
|
|
});
|
|
});
|
|
window.postMessage(
|
|
{
|
|
type,
|
|
data,
|
|
},
|
|
"*"
|
|
);
|
|
|
|
return messageReceivedPromise;
|
|
}
|
|
|
|
async function expectEventTypeInfoInCurrentRouting({
|
|
eventTypeText,
|
|
eventTypeHref,
|
|
}: {
|
|
eventTypeText: string;
|
|
eventTypeHref: string;
|
|
}) {
|
|
const eventTypeEl = screen.getByTestId("current-routing-status-event-type");
|
|
expect(eventTypeEl).toHaveTextContent(eventTypeText);
|
|
await expect(eventTypeEl.querySelector("a")).toHaveAttribute("href", eventTypeHref);
|
|
}
|
|
|
|
async function expectEventTypeInfoInReroutePreview({
|
|
eventTypeText,
|
|
eventTypeHref,
|
|
}: {
|
|
eventTypeText: string;
|
|
eventTypeHref: string;
|
|
}) {
|
|
const eventTypeEl = screen.getByTestId("reroute-preview-event-type");
|
|
expect(eventTypeEl).toHaveTextContent(eventTypeText);
|
|
await expect(eventTypeEl.querySelector("a")).toHaveAttribute("href", eventTypeHref);
|
|
}
|
|
|
|
function expectOrganizerInfoInCurrentRouting({ organizerText }: { organizerText: string }) {
|
|
const organizerEl = screen.getByTestId("current-routing-status-organizer");
|
|
expect(organizerEl).toHaveTextContent(organizerText);
|
|
}
|
|
|
|
function expectAttendeesInfoInCurrentRouting({ attendeesText }: { attendeesText: string }) {
|
|
const attendeesEl = screen.getByTestId("current-routing-status-attendees");
|
|
expect(attendeesEl).toHaveTextContent(attendeesText);
|
|
}
|
|
const userWhoBooked = { id: 1, name: "Test User", email: "user@example.com" };
|
|
const mockBooking = {
|
|
id: 1,
|
|
uid: "original-booking-uid",
|
|
title: "Test Booking",
|
|
startTime: new Date(new Date().setDate(new Date().getDate() + 1)).toISOString(),
|
|
metadata: {},
|
|
responses: {},
|
|
attendees: [{ email: "attendee@example.com", name: "Attendee", timeZone: "UTC", locale: "en" }],
|
|
eventType: {
|
|
id: 1,
|
|
slug: "test-event",
|
|
team: { slug: "test-team" },
|
|
length: 60,
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
title: "Test Event",
|
|
},
|
|
user: userWhoBooked,
|
|
routedFromRoutingFormReponse: { id: 1 },
|
|
status: BookingStatus.ACCEPTED, // Add this line
|
|
};
|
|
|
|
const buildBooking = () => {
|
|
return {
|
|
...mockBooking,
|
|
};
|
|
};
|
|
|
|
describe("RerouteDialog", () => {
|
|
const mockSetIsOpenDialog = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
test("renders the dialog when open", () => {
|
|
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
|
|
|
expect(screen.getByText("reroute_booking")).toBeInTheDocument();
|
|
expect(screen.getByText("reroute_booking_description")).toBeInTheDocument();
|
|
});
|
|
|
|
test("doesn't render the dialog when closed", () => {
|
|
render(
|
|
<RerouteDialog isOpenDialog={false} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
|
);
|
|
|
|
expect(screen.queryByText("reroute_booking")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("displays current routing status", async () => {
|
|
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
|
expect(screen.getByText("current_routing_status")).toBeInTheDocument();
|
|
|
|
expectEventTypeInfoInCurrentRouting({
|
|
eventTypeText: "team/test-team/test-event",
|
|
eventTypeHref: "https://cal.com/team/test-team/test-event",
|
|
});
|
|
expectOrganizerInfoInCurrentRouting({
|
|
organizerText: "user@example.com",
|
|
});
|
|
expectAttendeesInfoInCurrentRouting({
|
|
attendeesText: "attendee@example.com",
|
|
});
|
|
// screen.logTestingPlaygroundURL()
|
|
});
|
|
|
|
test("verify_new_route button is enabled even when form fields are not filled", async () => {
|
|
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
|
await expect(screen.getByText("verify_new_route")).toBeEnabled();
|
|
});
|
|
|
|
test("disabledFields are passed to FormInputFields with value ['email'] - email field is disabled", async () => {
|
|
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
|
expect(screen.getByTestId("mock-form-field-disabled-fields-identifiers")).toHaveTextContent(/^email$/);
|
|
});
|
|
|
|
test("Expect form fields and name to be rendered", async () => {
|
|
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
|
expect(screen.getByText("Test Form")).toBeInTheDocument();
|
|
expect(screen.getByTestId("mock-form-field-company-size-label")).toHaveTextContent("Company Size");
|
|
expect(screen.getByTestId("mock-form-field-company-size-value")).toHaveTextContent("small");
|
|
expect(screen.getByTestId("mock-form-field-country-label")).toHaveTextContent("Country");
|
|
expect(screen.getByTestId("mock-form-field-country-value")).toHaveTextContent("usa");
|
|
});
|
|
|
|
test("cancel button closes the dialog", async () => {
|
|
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
|
|
|
expect(screen.getByText("cancel")).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByText("cancel"));
|
|
expect(mockSetIsOpenDialog).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
describe("New Routing tests", () => {
|
|
test("when verify_new_route is clicked, the form is submitted", async () => {
|
|
render(
|
|
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
|
);
|
|
fireEvent.click(screen.getByText("verify_new_route"));
|
|
|
|
expectEventTypeInfoInReroutePreview({
|
|
eventTypeText: "team/test-team/new-test-event",
|
|
eventTypeHref: "https://cal.com/team/test-team/new-test-event",
|
|
});
|
|
await expect(screen.getByText("verify_new_route")).toBeEnabled();
|
|
expect(screen.getByTestId("reroute-preview-hosts")).toHaveTextContent("reroute_preview_possible_host");
|
|
expect(screen.getByTestId("reroute-preview-hosts")).toHaveTextContent("matching-user-1@example.com");
|
|
|
|
expect(screen.getByText("reschedule_to_the_new_event_with_different_timeslot")).toBeInTheDocument();
|
|
expect(screen.getByText("reschedule_with_same_timeslot_of_new_event")).toBeInTheDocument();
|
|
});
|
|
|
|
describe("New tab rescheduling", () => {
|
|
test("new tab is closed when new booking is rerouted", async () => {
|
|
render(
|
|
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
|
);
|
|
clickVerifyNewRouteButton();
|
|
clickRescheduleToTheNewEventWithDifferentTimeslotButton();
|
|
await mockMessageFromOpenedTab({
|
|
type: "CAL:rescheduleBookingSuccessfulV2",
|
|
data: {
|
|
uid: "RESCHEDULED_BOOKING_UID_NEW_TAB",
|
|
},
|
|
});
|
|
|
|
const rescheduleTabUrl = mockOpen.mock.calls[0][0] as unknown as string;
|
|
const rescheduleTabUrlObject = new URL(rescheduleTabUrl, "http://mockcal.com");
|
|
expect(Object.fromEntries(rescheduleTabUrlObject.searchParams.entries())).toEqual(
|
|
expect.objectContaining({
|
|
rescheduleUid: mockBooking.uid,
|
|
"cal.rerouting": "true",
|
|
// Shouldn't include the user who booked the booking
|
|
"cal.routedTeamMemberIds": "2",
|
|
"cal.reroutingFormResponses": JSON.stringify({
|
|
"company-size": {
|
|
value: "small",
|
|
},
|
|
country: {
|
|
value: "usa",
|
|
},
|
|
}),
|
|
})
|
|
);
|
|
expect(fakeNewTabWindow.close).toHaveBeenCalled();
|
|
expectToBeNavigatedToBookingPage({
|
|
booking: {
|
|
uid: "RESCHEDULED_BOOKING_UID_NEW_TAB",
|
|
},
|
|
});
|
|
});
|
|
|
|
test("Rescheduling with same timeslot works", async () => {
|
|
render(
|
|
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
|
);
|
|
clickVerifyNewRouteButton();
|
|
clickRescheduleWithSameTimeslotOfChosenEventButton();
|
|
expect(mockReactQueryMutateFn).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
rescheduleUid: mockBooking.uid,
|
|
// Shouldn't include the user who booked the booking
|
|
routedTeamMemberIds: [2],
|
|
reroutingFormResponses: {
|
|
"company-size": {
|
|
value: "small",
|
|
},
|
|
country: {
|
|
value: "usa",
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
expectToBeNavigatedToBookingPage({
|
|
booking: {
|
|
uid: "RESCHEDULED_BOOKING_UID_SAME_TIMESLOT",
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
function clickVerifyNewRouteButton() {
|
|
act(() => {
|
|
fireEvent.click(screen.getByText("verify_new_route"));
|
|
});
|
|
}
|
|
|
|
function clickRescheduleToTheNewEventWithDifferentTimeslotButton() {
|
|
act(() => {
|
|
fireEvent.click(screen.getByText("reschedule_to_the_new_event_with_different_timeslot"));
|
|
});
|
|
}
|
|
|
|
function clickRescheduleWithSameTimeslotOfChosenEventButton() {
|
|
act(() => {
|
|
fireEvent.click(screen.getByText("reschedule_with_same_timeslot_of_new_event"));
|
|
});
|
|
}
|
|
|
|
function expectToBeNavigatedToBookingPage({
|
|
booking,
|
|
}: {
|
|
booking: {
|
|
uid: string;
|
|
};
|
|
}) {
|
|
expect(mockRouter.push).toHaveBeenCalledWith(`/booking/${booking.uid}?cal.rerouting=true`);
|
|
}
|