Files
calendar/apps/web/playwright/duration-limits.e2e.ts
T
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* refactor: apply biome formatting to apps/web (batch 1)

Formats apps/web non-modules directories:
- apps/web/app
- apps/web/lib
- apps/web/pages
- apps/web/styles
- apps/web/server
- apps/web/test
- Root-level .ts and .mjs files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 2)

Formats smaller apps/web/modules directories:
- onboarding, data-table, users, apps, auth
- integration-attribute-sync, shell, availability
- feature-flags, team, webhooks, getting-started
- feature-opt-in, troubleshooter, schedules, notifications
- signup-view.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 3)

Formats medium apps/web/modules directories:
- bookings
- event-types
- insights
- embed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules/ee (batch 4)

Formats apps/web/modules/ee directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web (batch 5)

Formats remaining apps/web directories:
- apps/web/modules/settings
- apps/web/modules/booking-audit
- apps/web/playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/components (batch 6)

Formats remaining apps/web/components files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Formats newly added/modified files from main merge:
- WorkflowStepContainer.tsx (resolved merge conflicts)
- Newly moved files (blocklist, form-builder, schedules, etc.)
- Other files modified in main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 23:25:24 +05:30

481 lines
17 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/intervalLimits/intervalLimit";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { entries } from "@calcom/prisma/zod-utils";
import { test } from "./lib/fixtures";
import { bookTimeSlot, createUserWithLimits, expectSlotNotAllowedToBook } 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 separately
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);
// Find the first occurrence of the same weekday in the next month
// Note: .day() sets the day of the week for the CURRENT week, which can
// return a date in the previous month. Instead, we calculate the correct date.
const targetDay = date.day();
const startOfNextMonth = date.add(1, "month").startOf("month");
const firstDayOfMonth = startOfNextMonth.day();
let daysToAdd = targetDay - firstDayOfMonth;
if (daysToAdd < 0) daysToAdd += 7;
return startOfNextMonth.add(daysToAdd, "day");
};
const getLastEventUrlWithMonth = (user: Awaited<ReturnType<typeof createUserWithLimits>>, date: Dayjs) => {
return `/${user.username}/${user.eventTypes.at(-1)?.slug}?month=${date.format("YYYY-MM")}`;
};
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 expectSlotNotAllowedToBook(page);
});
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.describe("multiple limits", () => {
test("day limit with multiple limits set", async ({ page, users }) => {
const slug = "duration-limit-multiple-day";
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,
});
const bookingDate = firstMondayInBookingMonth;
const limitValue = BOOKING_LIMITS_MULTIPLE.PER_DAY;
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();
let slotUrl = "";
await test.step("can book up to day limit", async () => {
for (let i = 0; i < limitValue; 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);
}
});
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(-1);
// try to book directly via form page
await page.goto(slotUrl);
await expectSlotNotAllowedToBook(page);
});
});
test("week limit with multiple limits set", async ({ page, users, bookings }) => {
const slug = "duration-limit-multiple-week";
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,
});
const baseBookingDate = firstMondayInBookingMonth;
const eventTypeId = user.eventTypes.at(-1)?.id;
if (!eventTypeId) throw new Error("Event type not found");
// Pre-create 1 booking on Monday (same week as UI booking)
// This satisfies the daily limit for Monday and counts toward the weekly limit
const preBookingDate = baseBookingDate.hour(10).minute(0);
await bookings.create(user.id, user.username, eventTypeId, {
startTime: preBookingDate.toDate(),
endTime: preBookingDate.add(EVENT_LENGTH, "minutes").toDate(),
});
// Test week limit on Tuesday (same week, different day to avoid daily limit conflict)
// Need to book 1 more to hit weekly limit of 2 (1 already exists from pre-booking)
const bookingDate = baseBookingDate.add(1, "day");
const weekLimitValue = BOOKING_LIMITS_MULTIPLE.PER_WEEK;
const bookingsToMake = weekLimitValue - 1;
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();
let slotUrl = "";
await test.step("can book up to week limit", async () => {
for (let i = 0; i < bookingsToMake; 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);
}
});
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();
// After hitting weekly limit, all remaining days in the week should be blocked
// Monday was already blocked by daily limit, Tuesday is now blocked
// The remaining weekdays (Wed-Fri) should also be blocked = 3 more days
// Total blocked: 1 (Tuesday we just booked) + 3 (Wed-Fri) = 4 days
// But Monday was already blocked before we started, so delta is -4
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(-4);
// try to book directly via form page
await page.goto(slotUrl);
await expectSlotNotAllowedToBook(page);
});
});
test("month limit with multiple limits set", async ({ page, users, bookings }) => {
const slug = "duration-limit-multiple-month";
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,
});
const baseBookingDate = firstMondayInBookingMonth;
const eventTypeId = user.eventTypes.at(-1)?.id;
if (!eventTypeId) throw new Error("Event type not found");
for (let i = 0; i < 2; i++) {
const date = baseBookingDate.add(i, "day").hour(10).minute(0);
await bookings.create(user.id, user.username, eventTypeId, {
startTime: date.toDate(),
endTime: date.add(EVENT_LENGTH, "minutes").toDate(),
});
}
// Move to next week for month limit test (similar to booking-limits test)
const bookingDate = baseBookingDate.add(1, "week");
const monthLimitValue = BOOKING_LIMITS_MULTIPLE.PER_MONTH;
const bookingsToMake = monthLimitValue - 2;
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();
let slotUrl = "";
await test.step("can book up to month limit", async () => {
for (let i = 0; i < bookingsToMake; 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);
}
});
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(0);
// try to book directly via form page
await page.goto(slotUrl);
await expectSlotNotAllowedToBook(page);
});
});
test("year limit with multiple limits set", async ({ page, users, bookings }) => {
const slug = "duration-limit-multiple-year";
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,
});
const baseBookingDate = firstMondayInBookingMonth;
const eventTypeId = user.eventTypes.at(-1)?.id;
if (!eventTypeId) throw new Error("Event type not found");
const dates = [
baseBookingDate.hour(10).minute(0),
baseBookingDate.add(1, "day").hour(10).minute(0),
baseBookingDate.add(1, "week").hour(10).minute(0),
];
for (const date of dates) {
await bookings.create(user.id, user.username, eventTypeId, {
startTime: date.toDate(),
endTime: date.add(EVENT_LENGTH, "minutes").toDate(),
});
}
const yearLimitValue = BOOKING_LIMITS_MULTIPLE.PER_YEAR;
const bookingsToMake = yearLimitValue - 3;
const bookingDate = incrementDate(baseBookingDate, "month");
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();
let slotUrl = "";
await test.step("can book up to year limit", async () => {
for (let i = 0; i < bookingsToMake; 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);
}
});
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(0);
// try to book directly via form page
await page.goto(slotUrl);
await expectSlotNotAllowedToBook(page);
});
});
});
});