mirror of
https://github.com/hanzoai/calendar.git
synced 2026-09-24 22:42:24 +00:00
Both booking-sheet-keyboard.e2e.ts and bookings-list.e2e.ts intermittently
timeout in CI when clicking the booking item button before the DOM has
finished rendering. Adding explicit waitFor({ state: 'visible' }) on the
role=button element after the parent booking item is visible ensures the
click target is fully ready.
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
256 lines
7.2 KiB
TypeScript
256 lines
7.2 KiB
TypeScript
import { BookingStatus } from "@calcom/prisma/enums";
|
|
import type { Page } from "@playwright/test";
|
|
import { expect } from "@playwright/test";
|
|
|
|
import type { Fixtures } from "./lib/fixtures";
|
|
import { test } from "./lib/fixtures";
|
|
|
|
test.afterEach(({ users }) => users.deleteAll());
|
|
|
|
async function enableBookingsV3(prisma: Fixtures["prisma"]) {
|
|
const existing = await prisma.feature.findUnique({ where: { slug: "bookings-v3" } });
|
|
await prisma.feature.upsert({
|
|
where: { slug: "bookings-v3" },
|
|
update: { enabled: true },
|
|
create: { slug: "bookings-v3", enabled: true, type: "OPERATIONAL" },
|
|
});
|
|
return existing;
|
|
}
|
|
|
|
async function restoreBookingsV3(prisma: Fixtures["prisma"], existing: { enabled: boolean } | null) {
|
|
if (existing) {
|
|
await prisma.feature.update({
|
|
where: { slug: "bookings-v3" },
|
|
data: { enabled: existing.enabled },
|
|
});
|
|
} else {
|
|
await prisma.feature.deleteMany({ where: { slug: "bookings-v3" } });
|
|
}
|
|
}
|
|
|
|
async function createBooking({
|
|
bookingsFixture,
|
|
organizer,
|
|
organizerEventType,
|
|
attendees,
|
|
relativeDate = 0,
|
|
durationMins = 30,
|
|
title,
|
|
}: {
|
|
bookingsFixture: Fixtures["bookings"];
|
|
organizer: { id: number; username: string | null };
|
|
organizerEventType: { id: number };
|
|
attendees: { name: string; email: string; timeZone: string }[];
|
|
relativeDate?: number;
|
|
durationMins?: number;
|
|
title: string;
|
|
}) {
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
const bookingDurationMs = durationMins * 60 * 1000;
|
|
const startTime = new Date(Date.now() + relativeDate * DAY_MS);
|
|
const endTime = new Date(Date.now() + relativeDate * DAY_MS + bookingDurationMs);
|
|
return await bookingsFixture.create(organizer.id, organizer.username, organizerEventType.id, {
|
|
title,
|
|
status: BookingStatus.ACCEPTED,
|
|
startTime,
|
|
endTime,
|
|
attendees: {
|
|
createMany: {
|
|
data: [...attendees],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async function setupBookingsAndOpenSheet({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
bookingCount = 3,
|
|
}: {
|
|
page: Page;
|
|
users: Fixtures["users"];
|
|
bookings: Fixtures["bookings"];
|
|
prisma: Fixtures["prisma"];
|
|
bookingCount?: number;
|
|
}) {
|
|
const existingFlag = await enableBookingsV3(prisma);
|
|
const user = await users.create();
|
|
|
|
const fixtures = [];
|
|
for (let i = 0; i < bookingCount; i++) {
|
|
const fixture = await createBooking({
|
|
title: `Booking ${i + 1}`,
|
|
bookingsFixture: bookings,
|
|
relativeDate: i + 1,
|
|
organizer: user,
|
|
organizerEventType: user.eventTypes[0],
|
|
attendees: [{ name: `Attendee ${i + 1}`, email: `attendee${i + 1}@example.com`, timeZone: "Europe/Berlin" }],
|
|
});
|
|
fixtures.push(fixture);
|
|
}
|
|
|
|
await user.apiLogin();
|
|
const bookingsGetResponse = page.waitForResponse((response) =>
|
|
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
|
);
|
|
await page.goto("/bookings/upcoming", { waitUntil: "domcontentloaded" });
|
|
await bookingsGetResponse;
|
|
|
|
const firstBookingItem = page.locator(`[data-booking-uid="${fixtures[0].uid}"]`);
|
|
await expect(firstBookingItem).toBeVisible();
|
|
const firstButton = firstBookingItem.locator('[role="button"]').first();
|
|
await firstButton.waitFor({ state: "visible" });
|
|
await firstButton.click();
|
|
|
|
const sheet = page.locator('[role="dialog"]');
|
|
await expect(sheet).toBeVisible();
|
|
|
|
return { user, fixtures, existingFlag, sheet };
|
|
}
|
|
|
|
test.describe("Booking sheet keyboard shortcuts", () => {
|
|
test("ArrowDown navigates to the next booking", async ({ page, users, bookings, prisma }) => {
|
|
const { fixtures, existingFlag, sheet } = await setupBookingsAndOpenSheet({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
});
|
|
|
|
try {
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 1");
|
|
|
|
await page.keyboard.press("ArrowDown");
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 2");
|
|
} finally {
|
|
await restoreBookingsV3(prisma, existingFlag);
|
|
}
|
|
});
|
|
|
|
test("ArrowUp navigates to the previous booking", async ({ page, users, bookings, prisma }) => {
|
|
const { fixtures, existingFlag, sheet } = await setupBookingsAndOpenSheet({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
});
|
|
|
|
try {
|
|
await page.keyboard.press("ArrowDown");
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 2");
|
|
|
|
await page.keyboard.press("ArrowUp");
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 1");
|
|
} finally {
|
|
await restoreBookingsV3(prisma, existingFlag);
|
|
}
|
|
});
|
|
|
|
test("ArrowDown at last booking does NOT open the actions dropdown", async ({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
}) => {
|
|
const { fixtures, existingFlag, sheet } = await setupBookingsAndOpenSheet({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
bookingCount: 2,
|
|
});
|
|
|
|
try {
|
|
await page.keyboard.press("ArrowDown");
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 2");
|
|
|
|
await page.keyboard.press("ArrowDown");
|
|
|
|
await page.waitForTimeout(500);
|
|
const dropdownContent = page.locator('[data-radix-popper-content-wrapper]');
|
|
await expect(dropdownContent).toBeHidden();
|
|
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 2");
|
|
} finally {
|
|
await restoreBookingsV3(prisma, existingFlag);
|
|
}
|
|
});
|
|
|
|
test("ArrowUp at first booking does NOT open the actions dropdown", async ({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
}) => {
|
|
const { existingFlag, sheet } = await setupBookingsAndOpenSheet({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
});
|
|
|
|
try {
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 1");
|
|
|
|
await page.keyboard.press("ArrowUp");
|
|
|
|
await page.waitForTimeout(500);
|
|
const dropdownContent = page.locator('[data-radix-popper-content-wrapper]');
|
|
await expect(dropdownContent).toBeHidden();
|
|
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 1");
|
|
} finally {
|
|
await restoreBookingsV3(prisma, existingFlag);
|
|
}
|
|
});
|
|
|
|
test("Escape closes the sheet", async ({ page, users, bookings, prisma }) => {
|
|
const { existingFlag, sheet } = await setupBookingsAndOpenSheet({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
});
|
|
|
|
try {
|
|
await expect(sheet).toBeVisible();
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
await expect(sheet).toBeHidden();
|
|
} finally {
|
|
await restoreBookingsV3(prisma, existingFlag);
|
|
}
|
|
});
|
|
|
|
test("Rapid arrow key presses navigate correctly without triggering dropdown", async ({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
}) => {
|
|
const { fixtures, existingFlag, sheet } = await setupBookingsAndOpenSheet({
|
|
page,
|
|
users,
|
|
bookings,
|
|
prisma,
|
|
});
|
|
|
|
try {
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 1");
|
|
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowDown");
|
|
|
|
await expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 3");
|
|
|
|
const dropdownContent = page.locator('[data-radix-popper-content-wrapper]');
|
|
await expect(dropdownContent).toBeHidden();
|
|
} finally {
|
|
await restoreBookingsV3(prisma, existingFlag);
|
|
}
|
|
});
|
|
});
|