477 lines
16 KiB
TypeScript
477 lines
16 KiB
TypeScript
/**
|
|
* These e2e tests only aim to cover standard cases
|
|
* Edge cases are currently handled in integration tests only
|
|
*/
|
|
import { expect } from "@playwright/test";
|
|
|
|
import type { Dayjs } from "@calcom/dayjs";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
|
|
import prisma from "@calcom/prisma";
|
|
import { BookingStatus } from "@calcom/prisma/client";
|
|
import { entries } from "@calcom/prisma/zod-utils";
|
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
|
|
|
import { test } from "./lib/fixtures";
|
|
import { bookTimeSlot, createUserWithLimits } from "./lib/testUtils";
|
|
|
|
test.describe.configure({ mode: "parallel" });
|
|
test.afterEach(async ({ users }) => {
|
|
await users.deleteAll();
|
|
});
|
|
|
|
// used as a multiplier for duration limits
|
|
const EVENT_LENGTH = 30;
|
|
|
|
// limits used when testing each limit seperately
|
|
const BOOKING_LIMITS_SINGLE = {
|
|
PER_DAY: 2,
|
|
PER_WEEK: 2,
|
|
PER_MONTH: 2,
|
|
PER_YEAR: 2,
|
|
};
|
|
|
|
// limits used when testing multiple limits together
|
|
const BOOKING_LIMITS_MULTIPLE = {
|
|
PER_DAY: 1,
|
|
PER_WEEK: 2,
|
|
PER_MONTH: 3,
|
|
PER_YEAR: 4,
|
|
};
|
|
|
|
// prevent tests from crossing year boundaries - if currently in Oct or later, start booking in Jan instead of Nov
|
|
// (we increment months twice when checking multiple limits)
|
|
const firstDayInBookingMonth =
|
|
dayjs().month() >= 9 ? dayjs().add(1, "year").month(0).date(1) : dayjs().add(1, "month").date(1);
|
|
|
|
// avoid weekly edge cases
|
|
const firstMondayInBookingMonth = firstDayInBookingMonth.day(
|
|
firstDayInBookingMonth.date() === firstDayInBookingMonth.startOf("week").date() ? 1 : 8
|
|
);
|
|
|
|
// ensure we land on the same weekday when incrementing month
|
|
const incrementDate = (date: Dayjs, unit: dayjs.ManipulateType) => {
|
|
if (unit !== "month") return date.add(1, unit);
|
|
return date.add(1, "month").day(date.day());
|
|
};
|
|
|
|
const getLastEventUrlWithMonth = (user: Awaited<ReturnType<typeof createUserWithLimits>>, date: Dayjs) => {
|
|
return `/${user.username}/${user.eventTypes.at(-1)?.slug}?month=${date.format("YYYY-MM")}`;
|
|
};
|
|
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
test.skip("Booking limits", () => {
|
|
entries(BOOKING_LIMITS_SINGLE).forEach(([limitKey, bookingLimit]) => {
|
|
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
|
|
|
// test one limit at a time
|
|
test(limitUnit, async ({ page, users }) => {
|
|
const slug = `booking-limit-${limitUnit}`;
|
|
const singleLimit = { [limitKey]: bookingLimit };
|
|
|
|
const user = await createUserWithLimits({
|
|
users,
|
|
slug,
|
|
length: EVENT_LENGTH,
|
|
bookingLimits: singleLimit,
|
|
});
|
|
|
|
let slotUrl = "";
|
|
|
|
const monthUrl = getLastEventUrlWithMonth(user, firstMondayInBookingMonth);
|
|
await page.goto(monthUrl);
|
|
|
|
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
|
const bookingDay = availableDays.getByText(firstMondayInBookingMonth.date().toString(), {
|
|
exact: true,
|
|
});
|
|
|
|
// finish rendering days before counting
|
|
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
|
const availableDaysBefore = await availableDays.count();
|
|
|
|
let latestRescheduleUrl: string | null = null;
|
|
await test.step("can book up to limit", async () => {
|
|
for (let i = 0; i < bookingLimit; i++) {
|
|
await bookingDay.click();
|
|
|
|
await page.getByTestId("time").nth(0).click();
|
|
await bookTimeSlot(page);
|
|
|
|
slotUrl = page.url();
|
|
|
|
await expect(page.getByTestId("success-page")).toBeVisible();
|
|
latestRescheduleUrl = await page
|
|
.locator('span[data-testid="reschedule-link"] > a')
|
|
.getAttribute("href");
|
|
|
|
await page.goto(monthUrl);
|
|
}
|
|
});
|
|
|
|
const expectedAvailableDays = {
|
|
day: -1,
|
|
week: -5,
|
|
month: 0,
|
|
year: 0,
|
|
};
|
|
|
|
await test.step("but not over", async () => {
|
|
// should already have navigated to monthUrl - just ensure days are rendered
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
|
|
|
// ensure the day we just booked is now blocked
|
|
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
|
|
|
const availableDaysAfter = await availableDays.count();
|
|
|
|
// equals 0 if no available days, otherwise signed difference
|
|
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
|
expectedAvailableDays[limitUnit]
|
|
);
|
|
|
|
// try to book directly via form page
|
|
await page.goto(slotUrl);
|
|
await bookTimeSlot(page);
|
|
|
|
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
|
|
});
|
|
|
|
await test.step("but can reschedule", async () => {
|
|
const bookingId = latestRescheduleUrl?.split("/").pop();
|
|
const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: bookingId } });
|
|
|
|
const year = rescheduledBooking.startTime.getFullYear();
|
|
const month = String(rescheduledBooking.startTime.getMonth() + 1).padStart(2, "0");
|
|
const day = String(rescheduledBooking.startTime.getDate()).padStart(2, "0");
|
|
|
|
await page.goto(
|
|
`/${user.username}/${
|
|
user.eventTypes.at(-1)?.slug
|
|
}?rescheduleUid=${bookingId}&date=${year}-${month}-${day}&month=${year}-${month}`
|
|
);
|
|
|
|
const formerDay = availableDays.getByText(rescheduledBooking.startTime.getDate().toString(), {
|
|
exact: true,
|
|
});
|
|
await expect(formerDay).toBeVisible();
|
|
|
|
const formerTimeElement = page.locator('[data-testid="former_time_p"]');
|
|
await expect(formerTimeElement).toBeVisible();
|
|
|
|
await page.locator('[data-testid="time"]').nth(0).click();
|
|
|
|
await expect(page.locator('[name="name"]')).toBeDisabled();
|
|
await expect(page.locator('[name="email"]')).toBeDisabled();
|
|
|
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
|
|
|
await page.waitForLoadState("networkidle");
|
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
|
|
|
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: bookingId } });
|
|
expect(newBooking).not.toBeNull();
|
|
|
|
const updatedRescheduledBooking = await prisma.booking.findFirstOrThrow({
|
|
where: { uid: bookingId },
|
|
});
|
|
expect(updatedRescheduledBooking.status).toBe(BookingStatus.CANCELLED);
|
|
|
|
await prisma.booking.deleteMany({
|
|
where: {
|
|
id: {
|
|
in: [newBooking.id, rescheduledBooking.id],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
await test.step(`month after booking`, async () => {
|
|
await page.goto(getLastEventUrlWithMonth(user, firstMondayInBookingMonth.add(1, "month")));
|
|
|
|
// finish rendering days before counting
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
|
|
|
// the month after we made bookings should have availability unless we hit a yearly limit
|
|
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
|
});
|
|
});
|
|
});
|
|
|
|
test("multiple", async ({ page, users }) => {
|
|
const slug = "booking-limit-multiple";
|
|
|
|
const user = await createUserWithLimits({
|
|
users,
|
|
slug,
|
|
length: EVENT_LENGTH,
|
|
bookingLimits: BOOKING_LIMITS_MULTIPLE,
|
|
});
|
|
|
|
let slotUrl = "";
|
|
|
|
let bookingDate = firstMondayInBookingMonth;
|
|
|
|
// keep track of total bookings across multiple limits
|
|
let bookingCount = 0;
|
|
|
|
for (const [limitKey, limitValue] of entries(BOOKING_LIMITS_MULTIPLE)) {
|
|
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
|
|
|
const monthUrl = getLastEventUrlWithMonth(user, bookingDate);
|
|
await page.goto(monthUrl);
|
|
|
|
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
|
const bookingDay = availableDays.getByText(bookingDate.date().toString(), { exact: true });
|
|
|
|
// finish rendering days before counting
|
|
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
|
|
|
const availableDaysBefore = await availableDays.count();
|
|
|
|
await test.step(`can book up ${limitUnit} to limit`, async () => {
|
|
for (let i = 0; i + bookingCount < limitValue; i++) {
|
|
await bookingDay.click();
|
|
|
|
await page.getByTestId("time").nth(0).click();
|
|
await bookTimeSlot(page);
|
|
bookingCount++;
|
|
|
|
slotUrl = page.url();
|
|
|
|
await expect(page.getByTestId("success-page")).toBeVisible();
|
|
|
|
await page.goto(monthUrl);
|
|
}
|
|
});
|
|
|
|
const expectedAvailableDays = {
|
|
day: -1,
|
|
week: -4, // one day will already be blocked by daily limit
|
|
month: 0,
|
|
year: 0,
|
|
};
|
|
|
|
await test.step("but not over", async () => {
|
|
// should already have navigated to monthUrl - just ensure days are rendered
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
|
|
|
// ensure the day we just booked is now blocked
|
|
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
|
|
|
const availableDaysAfter = await availableDays.count();
|
|
|
|
// equals 0 if no available days, otherwise signed difference
|
|
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
|
expectedAvailableDays[limitUnit]
|
|
);
|
|
|
|
// try to book directly via form page
|
|
await page.goto(slotUrl);
|
|
await bookTimeSlot(page);
|
|
|
|
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
await test.step(`month after booking`, async () => {
|
|
await page.goto(getLastEventUrlWithMonth(user, bookingDate.add(1, "month")));
|
|
|
|
// finish rendering days before counting
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
|
|
|
// the month after we made bookings should have availability unless we hit a yearly limit
|
|
// TODO: Temporary fix for failing test. It passes locally but fails on CI.
|
|
// See #13097
|
|
// await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
|
});
|
|
|
|
// increment date by unit after hitting each limit
|
|
bookingDate = incrementDate(bookingDate, limitUnit);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe("Duration limits", () => {
|
|
entries(BOOKING_LIMITS_SINGLE).forEach(([limitKey, bookingLimit]) => {
|
|
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
|
|
|
// test one limit at a time
|
|
test(limitUnit, async ({ page, users }) => {
|
|
const slug = `duration-limit-${limitUnit}`;
|
|
const singleLimit = { [limitKey]: bookingLimit * EVENT_LENGTH };
|
|
|
|
const user = await createUserWithLimits({
|
|
users,
|
|
slug,
|
|
length: EVENT_LENGTH,
|
|
durationLimits: singleLimit,
|
|
});
|
|
|
|
let slotUrl = "";
|
|
|
|
const monthUrl = getLastEventUrlWithMonth(user, firstMondayInBookingMonth);
|
|
await page.goto(monthUrl);
|
|
|
|
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
|
const bookingDay = availableDays.getByText(firstMondayInBookingMonth.date().toString(), {
|
|
exact: true,
|
|
});
|
|
|
|
// finish rendering days before counting
|
|
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
|
const availableDaysBefore = await availableDays.count();
|
|
|
|
await test.step("can book up to limit", async () => {
|
|
for (let i = 0; i < bookingLimit; i++) {
|
|
await bookingDay.click();
|
|
|
|
await page.getByTestId("time").nth(0).click();
|
|
await bookTimeSlot(page);
|
|
|
|
slotUrl = page.url();
|
|
|
|
await expect(page.getByTestId("success-page")).toBeVisible();
|
|
|
|
await page.goto(monthUrl);
|
|
}
|
|
});
|
|
|
|
const expectedAvailableDays = {
|
|
day: -1,
|
|
week: -5,
|
|
month: 0,
|
|
year: 0,
|
|
};
|
|
|
|
await test.step("but not over", async () => {
|
|
// should already have navigated to monthUrl - just ensure days are rendered
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
|
|
|
// ensure the day we just booked is now blocked
|
|
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
|
|
|
const availableDaysAfter = await availableDays.count();
|
|
|
|
// equals 0 if no available days, otherwise signed difference
|
|
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
|
expectedAvailableDays[limitUnit]
|
|
);
|
|
|
|
// try to book directly via form page
|
|
await page.goto(slotUrl);
|
|
await bookTimeSlot(page);
|
|
|
|
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
|
|
});
|
|
|
|
await test.step(`month after booking`, async () => {
|
|
await page.goto(getLastEventUrlWithMonth(user, firstMondayInBookingMonth.add(1, "month")));
|
|
|
|
// finish rendering days before counting
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
|
|
|
// the month after we made bookings should have availability unless we hit a yearly limit
|
|
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
|
});
|
|
});
|
|
});
|
|
|
|
test("multiple", async ({ page, users }) => {
|
|
const slug = "duration-limit-multiple";
|
|
|
|
// multiply all booking limits by EVENT_LENGTH
|
|
const durationLimits = entries(BOOKING_LIMITS_MULTIPLE).reduce((limits, [limitKey, bookingLimit]) => {
|
|
return {
|
|
...limits,
|
|
[limitKey]: bookingLimit * EVENT_LENGTH,
|
|
};
|
|
}, {} as Record<keyof IntervalLimit, number>);
|
|
|
|
const user = await createUserWithLimits({
|
|
users,
|
|
slug,
|
|
length: EVENT_LENGTH,
|
|
durationLimits,
|
|
});
|
|
|
|
let slotUrl = "";
|
|
|
|
let bookingDate = firstMondayInBookingMonth;
|
|
|
|
// keep track of total bookings across multiple limits
|
|
let bookingCount = 0;
|
|
|
|
for (const [limitKey, limitValue] of entries(BOOKING_LIMITS_MULTIPLE)) {
|
|
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
|
|
|
const monthUrl = getLastEventUrlWithMonth(user, bookingDate);
|
|
await page.goto(monthUrl);
|
|
|
|
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
|
const bookingDay = availableDays.getByText(bookingDate.date().toString(), { exact: true });
|
|
|
|
// finish rendering days before counting
|
|
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
|
|
|
const availableDaysBefore = await availableDays.count();
|
|
|
|
await test.step(`can book up ${limitUnit} to limit`, async () => {
|
|
for (let i = 0; i + bookingCount < limitValue; i++) {
|
|
await bookingDay.click();
|
|
|
|
await page.getByTestId("time").nth(0).click();
|
|
await bookTimeSlot(page);
|
|
bookingCount++;
|
|
|
|
slotUrl = page.url();
|
|
|
|
await expect(page.getByTestId("success-page")).toBeVisible();
|
|
|
|
await page.goto(monthUrl);
|
|
}
|
|
});
|
|
|
|
const expectedAvailableDays = {
|
|
day: -1,
|
|
week: -4, // one day will already be blocked by daily limit
|
|
month: 0,
|
|
year: 0,
|
|
};
|
|
|
|
await test.step("but not over", async () => {
|
|
// should already have navigated to monthUrl - just ensure days are rendered
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
|
|
|
// ensure the day we just booked is now blocked
|
|
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
|
|
|
const availableDaysAfter = await availableDays.count();
|
|
|
|
// equals 0 if no available days, otherwise signed difference
|
|
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
|
expectedAvailableDays[limitUnit]
|
|
);
|
|
|
|
// try to book directly via form page
|
|
await page.goto(slotUrl);
|
|
await bookTimeSlot(page);
|
|
|
|
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
|
|
});
|
|
|
|
await test.step(`month after booking`, async () => {
|
|
await page.goto(getLastEventUrlWithMonth(user, bookingDate.add(1, "month")));
|
|
|
|
// finish rendering days before counting
|
|
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
|
|
|
// the month after we made bookings should have availability unless we hit a yearly limit
|
|
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
|
});
|
|
|
|
// increment date by unit after hitting each limit
|
|
bookingDate = incrementDate(bookingDate, limitUnit);
|
|
}
|
|
});
|
|
});
|