* use booking.uid instead of booking.id for url param * show timezone on calendar * fix type * restore horizontal tab and remove header and subtitle * clean up sidebar items * fix event propagation from attendees * fetch all statuses except for cancelled on calendar view * clean up styles of the badges on BookingListItem * fix useMediaQuery * add close button to the header * add assignment reason to the details sheet * use separator row * use ToggleGroup for the top bookings tab * move ViewToggleButton * resize the action button * remove wrong prop * fix type error * fix type error * hide view toggle button on mobile (and fix the breakpoint) * remove unused e2e tests * fix e2e tests * hide toggle button when feature flag is off * update skeleton * improve attendees on booking list item and slide over * improve attendee dropdown * fix type error * move query to containers * select attendee email * infinite fetching for calendar view * update styles * fix compatibility * fix: add backward compatibility for status field in getAllUserBookings * increase calendar height * fix type error * support Member filter only for admin / owners * add debug log (TEMP) * add event border color * show Reject / Accept buttons on BookingDetailsSheet * move description section to the top * update When section * update style of Who section * add CancelBookingDialog WIP * fix CancelBookingDialog * increase clickable area * add schedule info section WIP * fix flaky reject button * fixing reschedule info WIP * add fromReschedule index to Booking * improve rescheduled information * improve reassignment * fix type error * fix unit test * respect user's weekStart value on the booking calendar view * update debug log * improve payment section * clean up * fix log message * reposition filters on list view * fix bookings controller api2 e2e test * clean up file by extracting logic into custom hooks * rename files * merge BookingCalendar into its container * extract logic into separate hook files * remove redundant logic * rearrange items on calendar view * add WeekPicker * extract filter button * responsive header on list view * horizontal scroll for ToggleGroup WIP * fix type error * fix cancelling recurring event * address feedback * fix e2e tests * fix unit test * fix e2e tests * make hover style more visible for ToggleGroup * fix margin on CancelBookingDialog * update styles on the slide over (mostly font weight) * update style of CancelBookingDialog * update styles * update margin top for the header * refactor getBookingDetails handler * fix gap in who section * auto-filter the current user on the calendar view * calculate calendar height considering top banners * improve booking details sheet interaction without overlay * update calendar event styles * update reject dialog style * put uid first in the query params * fix class name * memoize functions in useMediaQuery * query attendee with id instead of email * update margins * replace TRPCError with ErrorWithCode * move calculation outside loop * remove dead code
554 lines
18 KiB
TypeScript
554 lines
18 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
|
|
|
import {
|
|
getPendingActions,
|
|
getCancelEventAction,
|
|
getVideoOptionsActions,
|
|
getEditEventActions,
|
|
getAfterEventActions,
|
|
shouldShowPendingActions,
|
|
shouldShowEditActions,
|
|
shouldShowRecurringCancelAction,
|
|
isActionDisabled,
|
|
getActionLabel,
|
|
type BookingActionContext,
|
|
} from "./bookingActions";
|
|
|
|
const mockT = (key: string) => key;
|
|
|
|
function createMockContext(overrides: Partial<BookingActionContext> = {}): BookingActionContext {
|
|
const now = new Date();
|
|
const startTime = new Date(now.getTime() + 24 * 60 * 60 * 1000); // Tomorrow
|
|
const endTime = new Date(startTime.getTime() + 60 * 60 * 1000); // 1 hour later
|
|
|
|
return {
|
|
booking: {
|
|
id: 1,
|
|
uid: "booking-123",
|
|
title: "Test Meeting",
|
|
description: "Test meeting description",
|
|
startTime: startTime.toISOString(),
|
|
endTime: endTime.toISOString(),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
status: BookingStatus.ACCEPTED,
|
|
paid: false,
|
|
fromReschedule: null,
|
|
recurringEventId: null,
|
|
rescheduled: false,
|
|
isRecorded: false,
|
|
rescheduler: null,
|
|
userPrimaryEmail: "organizer@example.com",
|
|
customInputs: {},
|
|
responses: {},
|
|
references: [],
|
|
attendees: [
|
|
{
|
|
id: 1,
|
|
name: "John Doe",
|
|
email: "john@example.com",
|
|
timeZone: "America/New_York",
|
|
phoneNumber: null,
|
|
locale: "en",
|
|
bookingId: 1,
|
|
noShow: false,
|
|
},
|
|
],
|
|
user: {
|
|
id: 1,
|
|
name: "Organizer",
|
|
email: "organizer@example.com",
|
|
},
|
|
eventType: {
|
|
id: 1,
|
|
title: "Test Event Type",
|
|
slug: "test-event",
|
|
length: 60,
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
team: null,
|
|
eventTypeColor: {
|
|
lightEventTypeColor: "#000000",
|
|
darkEventTypeColor: "#ffffff",
|
|
},
|
|
disableCancelling: false,
|
|
disableRescheduling: false,
|
|
disableGuests: false,
|
|
allowReschedulingPastBookings: false,
|
|
recurringEvent: null,
|
|
price: 0,
|
|
currency: "usd",
|
|
metadata: {},
|
|
},
|
|
location: "integrations:daily",
|
|
payment: [],
|
|
seatsReferences: [],
|
|
assignmentReason: [],
|
|
metadata: null,
|
|
routedFromRoutingFormReponse: null,
|
|
listingStatus: "upcoming",
|
|
recurringInfo: undefined,
|
|
loggedInUser: {
|
|
userId: 1,
|
|
userTimeZone: "America/New_York",
|
|
userTimeFormat: 12,
|
|
userEmail: "organizer@example.com",
|
|
},
|
|
isToday: false,
|
|
},
|
|
isUpcoming: true,
|
|
isOngoing: false,
|
|
isBookingInPast: false,
|
|
isCancelled: false,
|
|
isConfirmed: true,
|
|
isRejected: false,
|
|
isPending: false,
|
|
isRescheduled: false,
|
|
isRecurring: false,
|
|
isTabRecurring: false,
|
|
isTabUnconfirmed: false,
|
|
isBookingFromRoutingForm: false,
|
|
isDisabledCancelling: false,
|
|
isDisabledRescheduling: false,
|
|
isCalVideoLocation: true,
|
|
showPendingPayment: false,
|
|
cardCharged: false,
|
|
attendeeList: [
|
|
{
|
|
name: "John Doe",
|
|
email: "john@example.com",
|
|
id: 1,
|
|
noShow: false,
|
|
phoneNumber: null,
|
|
},
|
|
],
|
|
getSeatReferenceUid: () => undefined,
|
|
t: mockT,
|
|
...overrides,
|
|
} as BookingActionContext;
|
|
}
|
|
|
|
describe("Booking Actions", () => {
|
|
describe("getPendingActions", () => {
|
|
it("should return reject action for pending booking", () => {
|
|
const context = createMockContext({ isPending: true });
|
|
const actions = getPendingActions(context);
|
|
|
|
expect(actions).toHaveLength(2);
|
|
expect(actions[0]).toEqual({
|
|
id: "confirm",
|
|
bookingUid: "booking-123",
|
|
label: "confirm",
|
|
icon: "check",
|
|
disabled: false,
|
|
});
|
|
expect(actions[1]).toEqual({
|
|
id: "reject",
|
|
label: "reject",
|
|
icon: "ban",
|
|
disabled: false,
|
|
});
|
|
});
|
|
|
|
it("should return reject action only for non-pending booking", () => {
|
|
const context = createMockContext({ isPending: false });
|
|
const actions = getPendingActions(context);
|
|
|
|
expect(actions).toHaveLength(1);
|
|
expect(actions[0].id).toBe("reject");
|
|
});
|
|
|
|
it("should use correct labels for recurring bookings", () => {
|
|
const context = createMockContext({
|
|
isPending: true,
|
|
isRecurring: true,
|
|
isTabRecurring: true,
|
|
});
|
|
const actions = getPendingActions(context);
|
|
|
|
expect(actions[0].label).toBe("confirm_all");
|
|
expect(actions[1].label).toBe("reject_all");
|
|
});
|
|
});
|
|
|
|
describe("getCancelEventAction", () => {
|
|
it("should return cancel action with correct properties", () => {
|
|
const context = createMockContext();
|
|
const action = getCancelEventAction(context);
|
|
|
|
expect(action).toEqual({
|
|
id: "cancel",
|
|
label: "cancel_event",
|
|
icon: "circle-x",
|
|
color: "destructive",
|
|
disabled: false,
|
|
bookingUid: "booking-123",
|
|
});
|
|
});
|
|
|
|
it("should be disabled when cancellation is disabled", () => {
|
|
const context = createMockContext({ isDisabledCancelling: true });
|
|
const action = getCancelEventAction(context);
|
|
|
|
expect(action.disabled).toBe(true);
|
|
});
|
|
|
|
it("should be disabled for past pending bookings", () => {
|
|
const context = createMockContext({
|
|
isBookingInPast: true,
|
|
isPending: true,
|
|
isConfirmed: false,
|
|
});
|
|
const action = getCancelEventAction(context);
|
|
|
|
expect(action.disabled).toBe(true);
|
|
});
|
|
|
|
it("should include recurring parameters for recurring bookings", () => {
|
|
const context = createMockContext({
|
|
isRecurring: true,
|
|
isTabRecurring: true,
|
|
});
|
|
const action = getCancelEventAction(context);
|
|
|
|
expect(action.label).toBe("cancel_all_remaining");
|
|
});
|
|
});
|
|
|
|
describe("getVideoOptionsActions", () => {
|
|
it("should return video actions for past confirmed bookings", () => {
|
|
const context = createMockContext({
|
|
isBookingInPast: true,
|
|
isConfirmed: true,
|
|
isCalVideoLocation: true,
|
|
booking: {
|
|
...createMockContext().booking,
|
|
isRecorded: true,
|
|
},
|
|
});
|
|
const actions = getVideoOptionsActions(context);
|
|
|
|
expect(actions).toHaveLength(2);
|
|
expect(actions[0].id).toBe("view_recordings");
|
|
expect(actions[1].id).toBe("meeting_session_details");
|
|
expect(actions[0].disabled).toBe(false);
|
|
expect(actions[1].disabled).toBe(false);
|
|
});
|
|
|
|
it("should disable video actions for upcoming bookings", () => {
|
|
const context = createMockContext({ isBookingInPast: false });
|
|
const actions = getVideoOptionsActions(context);
|
|
|
|
expect(actions[0].disabled).toBe(true);
|
|
expect(actions[1].disabled).toBe(true);
|
|
});
|
|
|
|
it("should disable video actions for non-Cal video locations", () => {
|
|
const context = createMockContext({
|
|
isBookingInPast: true,
|
|
isConfirmed: true,
|
|
isCalVideoLocation: false,
|
|
});
|
|
const actions = getVideoOptionsActions(context);
|
|
|
|
expect(actions[0].disabled).toBe(true);
|
|
expect(actions[1].disabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getEditEventActions", () => {
|
|
it("should return basic edit actions", () => {
|
|
const context = createMockContext();
|
|
const actions = getEditEventActions(context);
|
|
|
|
const actionIds = actions.map((a) => a.id);
|
|
expect(actionIds).toContain("reschedule");
|
|
expect(actionIds).toContain("reschedule_request");
|
|
expect(actionIds).toContain("change_location");
|
|
expect(actionIds).toContain("add_members");
|
|
});
|
|
|
|
it("should include reroute action for routing form bookings", () => {
|
|
const context = createMockContext({ isBookingFromRoutingForm: true });
|
|
const actions = getEditEventActions(context);
|
|
|
|
const rerouteAction = actions.find((a) => a.id === "reroute");
|
|
expect(rerouteAction).toBeDefined();
|
|
});
|
|
|
|
it("should include reassign action for round robin events with no host groups", () => {
|
|
const context = createMockContext({
|
|
booking: {
|
|
...createMockContext().booking,
|
|
eventType: {
|
|
...createMockContext().booking.eventType,
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
hostGroups: [],
|
|
},
|
|
},
|
|
});
|
|
const actions = getEditEventActions(context);
|
|
|
|
const reassignAction = actions.find((a) => a.id === "reassign");
|
|
expect(reassignAction).toBeDefined();
|
|
});
|
|
|
|
it("should include reassign action for round robin events with one host group", () => {
|
|
const context = createMockContext({
|
|
booking: {
|
|
...createMockContext().booking,
|
|
eventType: {
|
|
...createMockContext().booking.eventType,
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
hostGroups: [{ id: "group-1", name: "Group 1" }],
|
|
},
|
|
},
|
|
});
|
|
const actions = getEditEventActions(context);
|
|
|
|
const reassignAction = actions.find((a) => a.id === "reassign");
|
|
expect(reassignAction).toBeDefined();
|
|
});
|
|
|
|
it("should exclude reassign action for round robin events with more than one host groups", () => {
|
|
const context = createMockContext({
|
|
booking: {
|
|
...createMockContext().booking,
|
|
eventType: {
|
|
...createMockContext().booking.eventType,
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
hostGroups: [
|
|
{ id: "group-1", name: "Group 1" },
|
|
{ id: "group-2", name: "Group 2" },
|
|
],
|
|
},
|
|
},
|
|
});
|
|
const actions = getEditEventActions(context);
|
|
|
|
const reassignAction = actions.find((a) => a.id === "reassign");
|
|
expect(reassignAction).toBeUndefined();
|
|
});
|
|
|
|
it("should exclude reassign action for non-round-robin events", () => {
|
|
const context = createMockContext({
|
|
booking: {
|
|
...createMockContext().booking,
|
|
eventType: {
|
|
...createMockContext().booking.eventType,
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
hostGroups: [],
|
|
},
|
|
},
|
|
});
|
|
const actions = getEditEventActions(context);
|
|
|
|
const reassignAction = actions.find((a) => a.id === "reassign");
|
|
expect(reassignAction).toBeUndefined();
|
|
});
|
|
|
|
it("should exclude add_members when guests are disabled", () => {
|
|
const context = createMockContext({
|
|
booking: {
|
|
...createMockContext().booking,
|
|
eventType: {
|
|
...createMockContext().booking.eventType,
|
|
disableGuests: true,
|
|
},
|
|
},
|
|
});
|
|
const actions = getEditEventActions(context);
|
|
|
|
const addMembersAction = actions.find((a) => a.id === "add_members");
|
|
expect(addMembersAction).toBeUndefined();
|
|
});
|
|
|
|
it("should disable reschedule actions when rescheduling is disabled", () => {
|
|
const context = createMockContext({ isDisabledRescheduling: true });
|
|
const actions = getEditEventActions(context);
|
|
|
|
const rescheduleAction = actions.find((a) => a.id === "reschedule");
|
|
const rescheduleRequestAction = actions.find((a) => a.id === "reschedule_request");
|
|
|
|
expect(rescheduleAction?.disabled).toBe(true);
|
|
expect(rescheduleRequestAction?.disabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getAfterEventActions", () => {
|
|
it("should include video actions and no-show action", () => {
|
|
const context = createMockContext({ isBookingInPast: true, isConfirmed: true });
|
|
const actions = getAfterEventActions(context);
|
|
|
|
const actionIds = actions.map((a) => a.id);
|
|
expect(actionIds).toContain("view_recordings");
|
|
expect(actionIds).toContain("meeting_session_details");
|
|
expect(actionIds).toContain("no_show");
|
|
});
|
|
|
|
it("should include charge card action for held payments", () => {
|
|
const context = createMockContext({
|
|
booking: {
|
|
...createMockContext().booking,
|
|
status: BookingStatus.ACCEPTED,
|
|
paid: true,
|
|
payment: [
|
|
{
|
|
paymentOption: "HOLD",
|
|
amount: 1000,
|
|
currency: "usd",
|
|
success: true,
|
|
appId: null,
|
|
refunded: false,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
const actions = getAfterEventActions(context);
|
|
|
|
const chargeCardAction = actions.find((a) => a.id === "charge_card");
|
|
expect(chargeCardAction).toBeDefined();
|
|
});
|
|
|
|
it("should show correct no-show label for single attendee", () => {
|
|
const context = createMockContext({
|
|
attendeeList: [{ name: "John", email: "john@example.com", id: 1, noShow: true, phoneNumber: null }],
|
|
});
|
|
const actions = getAfterEventActions(context);
|
|
|
|
const noShowAction = actions.find((a) => a.id === "no_show");
|
|
expect(noShowAction?.label).toBe("unmark_as_no_show");
|
|
expect(noShowAction?.icon).toBe("eye");
|
|
});
|
|
});
|
|
|
|
describe("shouldShowPendingActions", () => {
|
|
it("should return true for pending upcoming bookings", () => {
|
|
const context = createMockContext({ isPending: true, isUpcoming: true, isCancelled: false });
|
|
expect(shouldShowPendingActions(context)).toBe(true);
|
|
});
|
|
|
|
it("should return false for cancelled bookings", () => {
|
|
const context = createMockContext({ isPending: true, isUpcoming: true, isCancelled: true });
|
|
expect(shouldShowPendingActions(context)).toBe(false);
|
|
});
|
|
|
|
it("should return false for past bookings", () => {
|
|
const context = createMockContext({ isPending: true, isUpcoming: false, isCancelled: false });
|
|
expect(shouldShowPendingActions(context)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("shouldShowEditActions", () => {
|
|
it("should return true for confirmed upcoming bookings", () => {
|
|
const context = createMockContext({ isPending: false, isCancelled: false });
|
|
expect(shouldShowEditActions(context)).toBe(true);
|
|
});
|
|
|
|
it("should return false for pending bookings", () => {
|
|
const context = createMockContext({ isPending: true });
|
|
expect(shouldShowEditActions(context)).toBe(false);
|
|
});
|
|
|
|
it("should return false for cancelled bookings", () => {
|
|
const context = createMockContext({ isCancelled: true });
|
|
expect(shouldShowEditActions(context)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("shouldShowRecurringCancelAction", () => {
|
|
it("should return true for recurring bookings in recurring tab", () => {
|
|
const context = createMockContext({ isTabRecurring: true, isRecurring: true });
|
|
expect(shouldShowRecurringCancelAction(context)).toBe(true);
|
|
});
|
|
|
|
it("should return false for non-recurring bookings", () => {
|
|
const context = createMockContext({ isTabRecurring: true, isRecurring: false });
|
|
expect(shouldShowRecurringCancelAction(context)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isActionDisabled", () => {
|
|
it("should disable reschedule actions when rescheduling is disabled", () => {
|
|
const context = createMockContext({ isDisabledRescheduling: true });
|
|
|
|
expect(isActionDisabled("reschedule", context)).toBe(true);
|
|
expect(isActionDisabled("reschedule_request", context)).toBe(true);
|
|
});
|
|
|
|
it("should disable cancel action when cancellation is disabled", () => {
|
|
const context = createMockContext({ isDisabledCancelling: true });
|
|
|
|
expect(isActionDisabled("cancel", context)).toBe(true);
|
|
});
|
|
|
|
it("should disable cancelling all past bookings", () => {
|
|
const pastConfirmedContext = createMockContext({
|
|
isBookingInPast: true,
|
|
isPending: false,
|
|
isConfirmed: true,
|
|
});
|
|
|
|
const pastPendingContext = createMockContext({
|
|
isBookingInPast: true,
|
|
isPending: true,
|
|
isConfirmed: false,
|
|
});
|
|
|
|
// Current implementation blocks ALL past bookings
|
|
expect(isActionDisabled("cancel", pastConfirmedContext)).toBe(true);
|
|
expect(isActionDisabled("cancel", pastPendingContext)).toBe(true);
|
|
});
|
|
|
|
it("should allow cancelling future bookings when cancelling is not disabled", () => {
|
|
const futureContext = createMockContext({
|
|
isBookingInPast: false,
|
|
isPending: true,
|
|
isConfirmed: false,
|
|
});
|
|
|
|
expect(isActionDisabled("cancel", futureContext)).toBe(false);
|
|
});
|
|
|
|
it("should disable video actions for non-past bookings", () => {
|
|
const context = createMockContext({ isBookingInPast: false });
|
|
|
|
expect(isActionDisabled("view_recordings", context)).toBe(true);
|
|
expect(isActionDisabled("meeting_session_details", context)).toBe(true);
|
|
});
|
|
|
|
it("should disable charge card action when already charged", () => {
|
|
const context = createMockContext({ cardCharged: true });
|
|
|
|
expect(isActionDisabled("charge_card", context)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getActionLabel", () => {
|
|
it("should return correct labels for different actions", () => {
|
|
const context = createMockContext();
|
|
|
|
expect(getActionLabel("reject", context)).toBe("reject");
|
|
expect(getActionLabel("confirm", context)).toBe("confirm");
|
|
expect(getActionLabel("cancel", context)).toBe("cancel_event");
|
|
});
|
|
|
|
it("should return correct labels for recurring bookings", () => {
|
|
const context = createMockContext({ isRecurring: true, isTabRecurring: true });
|
|
|
|
expect(getActionLabel("reject", context)).toBe("reject_all");
|
|
expect(getActionLabel("confirm", context)).toBe("confirm_all");
|
|
expect(getActionLabel("cancel", context)).toBe("cancel_all_remaining");
|
|
});
|
|
|
|
it("should return correct no-show label based on attendee state", () => {
|
|
const contextWithNoShow = createMockContext({
|
|
attendeeList: [{ name: "John", email: "john@example.com", id: 1, noShow: true, phoneNumber: null }],
|
|
});
|
|
|
|
expect(getActionLabel("no_show", contextWithNoShow)).toBe("unmark_as_no_show");
|
|
});
|
|
});
|
|
});
|