cal/packages/lib/slots.test.ts

346 lines
9.7 KiB
TypeScript

import { describe, expect, it, beforeAll, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { MINUTES_DAY_END, MINUTES_DAY_START } from "@calcom/lib/availability";
import type { DateRange } from "./date-ranges";
import getSlots from "./slots";
let dateRangesNextDay: DateRange[];
let dateRangesMockDay: DateRange[];
beforeAll(() => {
vi.setSystemTime(dayjs.utc("2021-06-20T11:59:59Z").toDate());
dateRangesMockDay = [{ start: dayjs.utc().startOf("day"), end: dayjs.utc().endOf("day") }];
dateRangesNextDay = [
{
start: dayjs.utc().add(1, "day").startOf("day"),
end: dayjs.utc().add(1, "day").endOf("day"),
},
];
});
describe("Tests the date-range slot logic", () => {
it("can fit 24 hourly slots for an empty day", async () => {
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 60,
minimumBookingNotice: 0,
eventLength: 60,
organizerTimeZone: "Etc/GMT",
dateRanges: dateRangesNextDay,
})
).toHaveLength(24);
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 60,
minimumBookingNotice: 0,
eventLength: 60,
organizerTimeZone: "America/Toronto",
dateRanges: dateRangesNextDay,
})
).toHaveLength(24);
});
it("only shows future booking slots on the same day", async () => {
// The mock date is 1s to midday, so 12 slots should be open given 0 booking notice.
expect(
getSlots({
inviteeDate: dayjs.utc(),
frequency: 60,
minimumBookingNotice: 0,
dateRanges: dateRangesMockDay,
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(12);
});
it("adds minimum booking notice correctly", async () => {
// 24h in a day.
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day").startOf("day"),
frequency: 60,
minimumBookingNotice: 1500,
dateRanges: dateRangesNextDay,
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(11);
});
it("shows correct time slots for 20 minutes long events with working hours that do not end at a full hour ", async () => {
// 72 20-minutes events in a 24h day
const result = getSlots({
inviteeDate: dayjs().add(1, "day"),
frequency: 20,
minimumBookingNotice: 0,
dateRanges: dateRangesNextDay,
eventLength: 20,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
});
expect(result).toHaveLength(72);
});
});
describe("Tests the slot logic", () => {
it("can fit 24 hourly slots for an empty day", async () => {
// 24h in a day.
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 60,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: MINUTES_DAY_START,
endTime: MINUTES_DAY_END,
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(24);
});
// TODO: This test is sound; it should pass!
it("only shows future booking slots on the same day", async () => {
// The mock date is 1s to midday, so 12 slots should be open given 0 booking notice.
expect(
getSlots({
inviteeDate: dayjs.utc(),
frequency: 60,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: MINUTES_DAY_START,
endTime: MINUTES_DAY_END,
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(12);
});
it("can cut off dates that due to invitee timezone differences fall on the next day", async () => {
expect(
getSlots({
inviteeDate: dayjs().tz("Europe/Amsterdam").startOf("day"), // time translation +01:00
frequency: 60,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: [0],
startTime: 23 * 60, // 23h
endTime: MINUTES_DAY_END,
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(0);
});
it("can cut off dates that due to invitee timezone differences fall on the previous day", async () => {
const workingHours = [
{
userId: 1,
days: [0],
startTime: MINUTES_DAY_START,
endTime: 1 * 60, // 1h
},
];
expect(
getSlots({
inviteeDate: dayjs().tz("Atlantic/Cape_Verde").startOf("day"), // time translation -01:00
frequency: 60,
minimumBookingNotice: 0,
workingHours,
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(0);
});
it("adds minimum booking notice correctly", async () => {
// 24h in a day.
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day").startOf("day"),
frequency: 60,
minimumBookingNotice: 1500,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: MINUTES_DAY_START,
endTime: MINUTES_DAY_END,
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(11);
});
it("shows correct time slots for 20 minutes long events with working hours that do not end at a full hour", async () => {
const result = getSlots({
inviteeDate: dayjs().add(1, "day"),
frequency: 20,
minimumBookingNotice: 0,
dateRanges: [{ start: dayjs("2021-06-21T00:00:00.000Z"), end: dayjs("2021-06-21T23:45:00.000Z") }],
/*workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: MINUTES_DAY_START,
endTime: MINUTES_DAY_END - 14, // 23:45
},
],*/
eventLength: 20,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
});
// 71 20-minutes events in a 24h - 15m day
expect(result).toHaveLength(71);
});
it("can fit 48 25 minute slots with a 5 minute offset for an empty day", async () => {
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 25,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: MINUTES_DAY_START,
endTime: MINUTES_DAY_END,
},
],
eventLength: 25,
offsetStart: 5,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(48);
});
it("tests the final slot of the day is included", async () => {
const slots = getSlots({
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000+02:00", "Europe/Brussels"),
eventLength: 15,
workingHours: [
{
days: [1, 2, 3, 4, 5],
startTime: 480,
endTime: 960,
userId: 9,
},
{
days: [4],
startTime: 1170,
endTime: 1379,
userId: 9,
},
],
dateOverrides: [],
offsetStart: 0,
dateRanges: [
{ start: dayjs("2023-07-13T07:00:00.000Z"), end: dayjs("2023-07-13T15:00:00.000Z") },
{ start: dayjs("2023-07-13T18:30:00.000Z"), end: dayjs("2023-07-13T20:59:59.000Z") },
],
minimumBookingNotice: 120,
frequency: 15,
organizerTimeZone: "Europe/London",
}).reverse();
expect(slots[0].time.format()).toBe("2023-07-13T22:45:00+02:00");
});
it("tests slots for half hour timezones", async () => {
const slots = getSlots({
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000+05:30", "Asia/Kolkata"),
frequency: 60,
minimumBookingNotice: 0,
eventLength: 60,
organizerTimeZone: "Asia/Kolkata",
dateRanges: [
{
start: dayjs.tz("2023-07-13T07:30:00.000", "Asia/Kolkata"),
end: dayjs.tz("2023-07-13T09:30:00.000", "Asia/Kolkata"),
},
],
});
expect(slots).toHaveLength(1);
expect(slots[0].time.format()).toBe("2023-07-13T08:00:00+05:30");
});
});
describe("Tests the date-range slot logic with custom env variable", () => {
beforeAll(() => {
vi.stubEnv("NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL", "10");
});
it("can fit 11 10 minute slots within a 2 hour window using a 10 mintue availabilty option with a starting time of 10 past the hour", async () => {
expect(Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL)).toBe(10);
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 10,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: 10,
endTime: 120,
},
],
eventLength: 10,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(11);
});
it("test buildSlotsWithDateRanges using a 10 mintue interval", async () => {
expect(Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL)).toBe(10);
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 10,
minimumBookingNotice: 0,
eventLength: 10,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
dateRanges: [{ start: dayjs("2023-07-13T00:10:00.000Z"), end: dayjs("2023-07-13T02:00:00.000Z") }],
})
).toHaveLength(11);
});
});