567 lines
20 KiB
TypeScript
567 lines
20 KiB
TypeScript
import { expect } from "@playwright/test";
|
|
import type { Prisma } from "@prisma/client";
|
|
import { uuid } from "short-uuid";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
import prisma from "@calcom/prisma";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
|
|
import type { Fixtures } from "./lib/fixtures";
|
|
import { test } from "./lib/fixtures";
|
|
import { testBothBookers } from "./lib/new-booker";
|
|
import {
|
|
bookTimeSlot,
|
|
createNewSeatedEventType,
|
|
selectFirstAvailableTimeSlotNextMonth,
|
|
} from "./lib/testUtils";
|
|
|
|
test.describe.configure({ mode: "parallel" });
|
|
test.afterEach(({ users }) => users.deleteAll());
|
|
|
|
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
|
|
const slug = "seats";
|
|
const user = await users.create({
|
|
eventTypes: [
|
|
{
|
|
title: "Seated event",
|
|
slug,
|
|
seatsPerTimeSlot: 10,
|
|
requiresConfirmation: true,
|
|
length: 30,
|
|
disableGuests: true, // should always be true for seated events
|
|
},
|
|
],
|
|
});
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const eventType = user.eventTypes.find((e) => e.slug === slug)!;
|
|
return { user, eventType };
|
|
}
|
|
|
|
async function createUserWithSeatedEventAndAttendees(
|
|
fixtures: Pick<Fixtures, "users" | "bookings">,
|
|
attendees: Prisma.AttendeeCreateManyBookingInput[]
|
|
) {
|
|
const { user, eventType } = await createUserWithSeatedEvent(fixtures.users);
|
|
const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, {
|
|
status: BookingStatus.ACCEPTED,
|
|
// startTime with 1 day from now and endTime half hour after
|
|
startTime: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
|
|
attendees: {
|
|
createMany: {
|
|
data: attendees,
|
|
},
|
|
},
|
|
});
|
|
return { user, eventType, booking };
|
|
}
|
|
|
|
testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
|
test("User can create a seated event (2 seats as example)", async ({ users, page }) => {
|
|
const user = await users.create({ name: "Seated event" });
|
|
await user.apiLogin();
|
|
await page.goto("/event-types");
|
|
// We wait until loading is finished
|
|
await page.waitForSelector('[data-testid="event-types"]');
|
|
const eventTitle = "My 2-seated event";
|
|
await createNewSeatedEventType(page, { eventTitle });
|
|
await expect(page.locator(`text=${eventTitle} event type updated successfully`)).toBeVisible();
|
|
});
|
|
|
|
test("Multiple Attendees can book a seated event time slot", async ({ users, page }) => {
|
|
const slug = "my-2-seated-event";
|
|
const user = await users.create({
|
|
name: "Seated event user",
|
|
eventTypes: [
|
|
{
|
|
title: "My 2-seated event",
|
|
slug,
|
|
length: 60,
|
|
seatsPerTimeSlot: 2,
|
|
seatsShowAttendees: true,
|
|
},
|
|
],
|
|
});
|
|
await page.goto(`/${user.username}/${slug}`);
|
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
|
|
|
// Kept in if statement here, since it's only temporary
|
|
// until the old booker isn't used anymore, and I wanted
|
|
// to change the test as little as possible.
|
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
|
if (bookerVariant === "old-booker") {
|
|
await page.waitForURL((url) => {
|
|
return url.pathname.endsWith("/book");
|
|
});
|
|
}
|
|
|
|
const bookingUrl = page.url();
|
|
await test.step("Attendee #1 can book a seated event time slot", async () => {
|
|
await page.goto(bookingUrl);
|
|
await bookTimeSlot(page);
|
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
|
});
|
|
await test.step("Attendee #2 can book the same seated event time slot", async () => {
|
|
await page.goto(bookingUrl);
|
|
await bookTimeSlot(page, { email: "jane.doe@example.com", name: "Jane Doe" });
|
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
|
});
|
|
await test.step("Attendee #3 cannot book the same seated event time slot", async () => {
|
|
await page.goto(bookingUrl);
|
|
await bookTimeSlot(page, { email: "rick@example.com", name: "Rick" });
|
|
await expect(page.locator("[data-testid=success-page]")).toBeHidden();
|
|
});
|
|
});
|
|
|
|
test(`Attendees can cancel a seated event time slot`, async ({ page, users, bookings }) => {
|
|
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
]);
|
|
|
|
const bookingAttendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
const bookingSeats = [
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[2].id, referenceUid: uuidv4() },
|
|
];
|
|
|
|
await prisma.bookingSeat.createMany({
|
|
data: bookingSeats,
|
|
});
|
|
|
|
await test.step("Attendee #1 should be able to cancel their booking", async () => {
|
|
await page.goto(`/booking/${booking.uid}?seatReferenceUid=${bookingSeats[0].referenceUid}`);
|
|
|
|
await page.locator('[data-testid="cancel"]').click();
|
|
await page.fill('[data-testid="cancel_reason"]', "Double booked!");
|
|
await page.locator('[data-testid="confirm_cancel"]').click();
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await expect(page).toHaveURL(/.*booking/);
|
|
|
|
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
|
|
await expect(cancelledHeadline).toBeVisible();
|
|
|
|
// Old booking should still exist, with one less attendee
|
|
const updatedBooking = await prisma.booking.findFirst({
|
|
where: { id: bookingSeats[0].bookingId },
|
|
include: { attendees: true },
|
|
});
|
|
|
|
const attendeeIds = updatedBooking?.attendees.map(({ id }) => id);
|
|
expect(attendeeIds).toHaveLength(2);
|
|
expect(attendeeIds).not.toContain(bookingAttendees[0].id);
|
|
});
|
|
|
|
await test.step("All attendees cancelling should delete the booking for the user", async () => {
|
|
// The remaining 2 attendees cancel
|
|
for (let i = 1; i < bookingSeats.length; i++) {
|
|
await page.goto(`/booking/${booking.uid}?seatReferenceUid=${bookingSeats[i].referenceUid}`);
|
|
|
|
await page.locator('[data-testid="cancel"]').click();
|
|
await page.fill('[data-testid="cancel_reason"]', "Double booked!");
|
|
await page.locator('[data-testid="confirm_cancel"]').click();
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await expect(page).toHaveURL(/.*booking/);
|
|
|
|
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
|
|
await expect(cancelledHeadline).toBeVisible();
|
|
}
|
|
|
|
// Should expect old booking to be cancelled
|
|
const updatedBooking = await prisma.booking.findFirst({
|
|
where: { id: bookingSeats[0].bookingId },
|
|
});
|
|
expect(updatedBooking).not.toBeNull();
|
|
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
|
|
});
|
|
});
|
|
});
|
|
|
|
testBothBookers.describe("Reschedule for booking with seats", () => {
|
|
test("Should reschedule booking with seats", async ({ page, users, bookings }) => {
|
|
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
|
{ name: "John First", email: `first+seats-${uuid()}@cal.com`, timeZone: "Europe/Berlin" },
|
|
{ name: "Jane Second", email: `second+seats-${uuid()}@cal.com`, timeZone: "Europe/Berlin" },
|
|
{ name: "John Third", email: `third+seats-${uuid()}@cal.com`, timeZone: "Europe/Berlin" },
|
|
]);
|
|
const bookingAttendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
},
|
|
});
|
|
|
|
const bookingSeats = [
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[2].id, referenceUid: uuidv4() },
|
|
];
|
|
|
|
await prisma.bookingSeat.createMany({
|
|
data: bookingSeats,
|
|
});
|
|
|
|
const references = await prisma.bookingSeat.findMany({
|
|
where: { bookingId: booking.id },
|
|
});
|
|
|
|
await page.goto(`/reschedule/${references[2].referenceUid}`);
|
|
|
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
|
|
|
// expect input to be filled with attendee number 3 data
|
|
const thirdAttendeeElement = await page.locator("input[name=name]");
|
|
const attendeeName = await thirdAttendeeElement.inputValue();
|
|
expect(attendeeName).toBe("John Third");
|
|
|
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
|
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await expect(page).toHaveURL(/.*booking/);
|
|
|
|
// Should expect new booking to be created for John Third
|
|
const newBooking = await prisma.booking.findFirst({
|
|
where: {
|
|
attendees: {
|
|
some: { email: bookingAttendees[2].email },
|
|
},
|
|
},
|
|
include: { seatsReferences: true, attendees: true },
|
|
});
|
|
expect(newBooking?.status).toBe(BookingStatus.PENDING);
|
|
expect(newBooking?.attendees.length).toBe(1);
|
|
expect(newBooking?.attendees[0].name).toBe("John Third");
|
|
expect(newBooking?.seatsReferences.length).toBe(1);
|
|
|
|
// Should expect old booking to be accepted with two attendees
|
|
const oldBooking = await prisma.booking.findFirst({
|
|
where: { uid: booking.uid },
|
|
include: { seatsReferences: true, attendees: true },
|
|
});
|
|
|
|
expect(oldBooking?.status).toBe(BookingStatus.ACCEPTED);
|
|
expect(oldBooking?.attendees.length).toBe(2);
|
|
expect(oldBooking?.seatsReferences.length).toBe(2);
|
|
});
|
|
|
|
test("Should reschedule booking with seats and if everyone rescheduled it should be deleted", async ({
|
|
page,
|
|
users,
|
|
bookings,
|
|
}) => {
|
|
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
]);
|
|
|
|
const bookingAttendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
const bookingSeats = [
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
|
|
];
|
|
|
|
await prisma.bookingSeat.createMany({
|
|
data: bookingSeats,
|
|
});
|
|
|
|
const references = await prisma.bookingSeat.findMany({
|
|
where: { bookingId: booking.id },
|
|
});
|
|
|
|
await page.goto(`/reschedule/${references[0].referenceUid}`);
|
|
|
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
|
|
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
|
|
|
await page.waitForURL(/.*booking/);
|
|
|
|
await page.goto(`/reschedule/${references[1].referenceUid}`);
|
|
|
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
|
|
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
|
|
|
// Using waitForUrl here fails the assertion `expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);` probably because waitForUrl is considered complete before waitForNavigation and till that time the booking is not cancelled
|
|
await page.waitForNavigation({ url: /.*booking/ });
|
|
|
|
// Should expect old booking to be cancelled
|
|
const oldBooking = await prisma.booking.findFirst({
|
|
where: { uid: booking.uid },
|
|
include: {
|
|
seatsReferences: true,
|
|
attendees: true,
|
|
eventType: {
|
|
include: { users: true, hosts: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);
|
|
});
|
|
|
|
test("Should cancel with seats and have no attendees and cancelled", async ({ page, users, bookings }) => {
|
|
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
]);
|
|
await user.apiLogin();
|
|
|
|
const oldBooking = await prisma.booking.findFirst({
|
|
where: { uid: booking.uid },
|
|
include: { seatsReferences: true, attendees: true },
|
|
});
|
|
|
|
const bookingAttendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
const bookingSeats = [
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
|
|
];
|
|
|
|
await prisma.bookingSeat.createMany({
|
|
data: bookingSeats,
|
|
});
|
|
|
|
// Now we cancel the booking as the organizer
|
|
await page.goto(`/booking/${booking.uid}?cancel=true`);
|
|
|
|
await page.locator('[data-testid="confirm_cancel"]').click();
|
|
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await expect(page).toHaveURL(/.*booking/);
|
|
|
|
// Should expect old booking to be cancelled
|
|
const updatedBooking = await prisma.booking.findFirst({
|
|
where: { uid: booking.uid },
|
|
include: { seatsReferences: true, attendees: true },
|
|
});
|
|
|
|
expect(oldBooking?.startTime).not.toBe(updatedBooking?.startTime);
|
|
});
|
|
|
|
test("If rescheduled/cancelled booking with seats it should display the correct number of seats", async ({
|
|
page,
|
|
users,
|
|
bookings,
|
|
}) => {
|
|
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
]);
|
|
|
|
const bookingAttendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
const bookingSeats = [
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
|
|
];
|
|
|
|
await prisma.bookingSeat.createMany({
|
|
data: bookingSeats,
|
|
});
|
|
|
|
const references = await prisma.bookingSeat.findMany({
|
|
where: { bookingId: booking.id },
|
|
});
|
|
|
|
await page.goto(
|
|
`/booking/${references[0].referenceUid}?cancel=true&seatReferenceUid=${references[0].referenceUid}`
|
|
);
|
|
|
|
await page.locator('[data-testid="confirm_cancel"]').click();
|
|
|
|
const oldBooking = await prisma.booking.findFirst({
|
|
where: { uid: booking.uid },
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
},
|
|
});
|
|
|
|
expect(oldBooking?.status).toBe(BookingStatus.ACCEPTED);
|
|
|
|
await page.goto(`/reschedule/${references[1].referenceUid}`);
|
|
|
|
await page.click('[data-testid="incrementMonth"]');
|
|
|
|
await page.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
|
|
|
|
// Validate that the number of seats its 10
|
|
expect(await page.locator("text=9 / 10 Seats available").count()).toEqual(0);
|
|
});
|
|
|
|
test("Should cancel with seats but event should be still accesible and with one less attendee/seat", async ({
|
|
page,
|
|
users,
|
|
bookings,
|
|
}) => {
|
|
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
]);
|
|
await user.apiLogin();
|
|
|
|
const bookingAttendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
const bookingSeats = [
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
|
|
];
|
|
|
|
await prisma.bookingSeat.createMany({
|
|
data: bookingSeats,
|
|
});
|
|
|
|
// Now we cancel the booking as the first attendee
|
|
// booking/${bookingUid}?cancel=true&allRemainingBookings=false&seatReferenceUid={bookingSeat.referenceUid}
|
|
await page.goto(
|
|
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[0].referenceUid}`
|
|
);
|
|
|
|
await page.locator('[data-testid="confirm_cancel"]').click();
|
|
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await expect(page).toHaveURL(/.*booking/);
|
|
|
|
await page.goto(
|
|
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[1].referenceUid}`
|
|
);
|
|
|
|
// Page should not be 404
|
|
await page.locator('[data-testid="confirm_cancel"]').click();
|
|
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
await expect(page).toHaveURL(/.*booking/);
|
|
});
|
|
|
|
test("Should book with seats and hide attendees info from showAttendees true", async ({
|
|
page,
|
|
users,
|
|
bookings,
|
|
}) => {
|
|
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
|
]);
|
|
await user.apiLogin();
|
|
const bookingWithEventType = await prisma.booking.findFirst({
|
|
where: { uid: booking.uid },
|
|
select: {
|
|
id: true,
|
|
eventTypeId: true,
|
|
},
|
|
});
|
|
|
|
await prisma.eventType.update({
|
|
data: {
|
|
seatsShowAttendees: false,
|
|
},
|
|
where: {
|
|
id: bookingWithEventType?.eventTypeId || -1,
|
|
},
|
|
});
|
|
|
|
const bookingAttendees = await prisma.attendee.findMany({
|
|
where: { bookingId: booking.id },
|
|
select: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
const bookingSeats = [
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
|
|
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
|
|
];
|
|
|
|
await prisma.bookingSeat.createMany({
|
|
data: bookingSeats,
|
|
});
|
|
|
|
// Go to cancel page and see that attendees are listed and myself as I'm owner of the booking
|
|
await page.goto(`/booking/${booking.uid}?cancel=true&allRemainingBookings=false`);
|
|
|
|
const foundFirstAttendeeAsOwner = await page.locator(
|
|
'p[data-testid="attendee-email-first+seats@cal.com"]'
|
|
);
|
|
await expect(foundFirstAttendeeAsOwner).toHaveCount(1);
|
|
const foundSecondAttendeeAsOwner = await page.locator(
|
|
'p[data-testid="attendee-email-second+seats@cal.com"]'
|
|
);
|
|
await expect(foundSecondAttendeeAsOwner).toHaveCount(1);
|
|
|
|
await page.goto("auth/logout");
|
|
|
|
// Now we cancel the booking as the first attendee
|
|
// booking/${bookingUid}?cancel=true&allRemainingBookings=false&seatReferenceUid={bookingSeat.referenceUid}
|
|
await page.goto(
|
|
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[0].referenceUid}`
|
|
);
|
|
|
|
// No attendees should be displayed only the one that it's cancelling
|
|
const notFoundSecondAttendee = await page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
|
|
|
|
await expect(notFoundSecondAttendee).toHaveCount(0);
|
|
const foundFirstAttendee = await page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
|
|
await expect(foundFirstAttendee).toHaveCount(1);
|
|
|
|
await prisma.eventType.update({
|
|
data: {
|
|
seatsShowAttendees: true,
|
|
},
|
|
where: {
|
|
id: bookingWithEventType?.eventTypeId || -1,
|
|
},
|
|
});
|
|
|
|
await page.goto(
|
|
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[1].referenceUid}`
|
|
);
|
|
|
|
// Now attendees should be displayed
|
|
const foundSecondAttendee = await page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
|
|
|
|
await expect(foundSecondAttendee).toHaveCount(1);
|
|
const foundFirstAttendeeAgain = await page
|
|
.locator('p[data-testid="attendee-email-first+seats@cal.com"]')
|
|
.first();
|
|
await expect(foundFirstAttendeeAgain).toHaveCount(1);
|
|
});
|
|
});
|