Files
calendar/apps/web/playwright/booking-seats.e2e.ts
T
8ad442f2be feat: Upgrade to Next 15 (#18834)
* wip

* update layoutHOC

* wip

* remove app router related code no longer used

* remove more

* await cookies, headers, params, serachparams

* update yarn.lock

* await cookies, headers, params, serachparams more

* update yarn lock again

* downgrade next-auth to 4.22.1

* update yarn lock

* fix

* update yarn lock

* fix type checks

* update yarn lock

* await headers and cookies

* restore pages folder

* restore yarn.lock

* update yarn.lock

* await headers and cookies

* remove

* await params in API routes

* updates

* restore next.config.js

* remove i18n from next.config.js

* Fixed tests

* Fixed types

* Removed duplicate favicon.ico

* Fixing more types

* ImageResponse moved to next/og

* Fixed prisma import issues

* dynamic import for @ewsjs/xhr

* remove deasync dep

* dynamic import for @tryvital/vital-node

* fix type checks

* add back turbopack command

* Type fix

* Removed unneeded file

* fix turbopack relative path errors

* add comments

* remove unneeded code

* Fixed build errors

* await apis

* use Promise<Params> type in defaultResponderForAppDir util

* refactor scim api route

* fix type checks

* separate app-routing.config into client-config and server-config

* wip

* refactor routing forms components

* revert unneeded changes for easier review

* fix

* fix

* use CustomTrans

* fix type

* fix unit tests

* fix type error

* fix build error

* fix build error

* fix build error

* fix warnings

* fix warnings

* upgrade @tremor/react and tailwindcss

* numCols -> numItems

* fix forgot-password e2e test

* fix 1 more e2e test

* fix login e2e test

* fix e2e tests

* fix e2e tests

* clean up

* remove error

* use tremor/react 2.11.0

* fix

* rename CustomTrans to ServerTrans

* address comment

* fix test

* fix ServerTrans

* fix

* fix type

* fix ServerTrans usages 1

* fix translations

* fix type checks

* fix type checks

* link styling

* fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-20 21:30:51 -03:00

463 lines
16 KiB
TypeScript

import { expect } from "@playwright/test";
import { v4 as uuidv4 } from "uuid";
import { randomString } from "@calcom/lib/random";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import {
confirmReschedule,
createNewSeatedEventType,
createUserWithSeatedEventAndAttendees,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test.describe("Booking with Seats", () => {
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=Event type updated successfully`)).toBeVisible();
});
test(`Prevent attendees from cancel when having invalid URL params`, 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,
name: true,
email: true,
},
});
const bookingSeats = bookingAttendees.map((attendee) => ({
bookingId: booking.id,
attendeeId: attendee.id,
referenceUid: uuidv4(),
data: {
responses: {
name: attendee.name,
email: attendee.email,
},
},
}));
await prisma.bookingSeat.createMany({
data: bookingSeats,
});
await test.step("Attendee #2 shouldn't be able to cancel booking using only booking/uid", async () => {
await page.goto(`/booking/${booking.uid}`);
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
});
await test.step("Attendee #2 shouldn't be able to cancel booking using randomString for seatReferenceUId", async () => {
await page.goto(`/booking/${booking.uid}?seatReferenceUid=${randomString(10)}`);
// expect cancel button to don't be in the page
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
});
});
test("Owner shouldn't be able to cancel booking without login in", async ({ page, bookings, users }) => {
const { booking, user } = 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" },
]);
await page.goto(`/booking/${booking.uid}?cancel=true`);
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
// expect login text to be in the page, not data-testid
await expect(page.locator("text=Login")).toHaveCount(1);
// click on login button text
await page.locator("text=Login").click();
// expect to be redirected to login page with query parameter callbackUrl
await expect(page).toHaveURL(/\/auth\/login\?callbackUrl=.*/);
await user.apiLogin();
// manual redirect to booking page
await page.goto(`/booking/${booking.uid}?cancel=true`);
// expect login button to don't be in the page
await expect(page.locator("text=Login")).toHaveCount(0);
// fill reason for cancellation
await page.fill('[data-testid="cancel_reason"]', "Double booked!");
// confirm cancellation
await page.locator('[data-testid="confirm_cancel"]').click();
await expect(page.locator("text=This event is canceled")).toBeVisible();
const updatedBooking = await prisma.booking.findFirst({
where: { id: booking.id },
});
expect(updatedBooking).not.toBeNull();
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
});
});
test.describe("Reschedule for booking with seats", () => {
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,
name: true,
email: true,
},
});
const bookingSeats = bookingAttendees.map((attendee) => ({
bookingId: booking.id,
attendeeId: attendee.id,
referenceUid: uuidv4(),
data: {
responses: {
name: attendee.name,
email: attendee.email,
},
},
}));
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 submitAndWaitForResponse(page, "/api/cancel", {
action: () => 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
await expect(page.locator("text=9 / 10 Seats available")).toHaveCount(0);
});
test("Should cancel with seats but event should be still accessible 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,
name: true,
email: true,
},
});
const bookingSeats = bookingAttendees.map((attendee) => ({
bookingId: booking.id,
attendeeId: attendee.id,
referenceUid: uuidv4(),
data: {
responses: {
name: attendee.name,
email: attendee.email,
},
},
}));
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.waitForSelector("text=Reason for cancellation");
await expect(page.locator('text="Cancel event"')).toBeVisible();
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
await page.locator('[data-testid="confirm_cancel"]').click();
await expect(page.locator("text=You are no longer attending this event")).toBeVisible();
await page.goto(
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[1].referenceUid}`
);
await page.locator('[data-testid="cancel_reason"]').fill("Test reason");
// Page should not be 404
await page.locator('[data-testid="confirm_cancel"]').click();
await expect(page.locator("text=You are no longer attending this event")).toBeVisible();
});
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,
name: true,
email: true,
},
});
const bookingSeats = bookingAttendees.map((attendee) => ({
bookingId: booking.id,
attendeeId: attendee.id,
referenceUid: uuidv4(),
data: {
responses: {
name: attendee.name,
email: attendee.email,
},
},
}));
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");
await page.getByTestId("logout-btn").click();
await expect(page).toHaveURL(/login/);
// 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);
});
test("Owner shouldn't be able to reschedule booking without login in", async ({
page,
bookings,
users,
}) => {
const { booking, user } = 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 getBooking = await booking.self();
await page.goto(`/booking/${booking.uid}`);
await expect(page.locator('[data-testid="reschedule"]')).toHaveCount(0);
// expect login text to be in the page, not data-testid
await expect(page.locator("text=Login")).toHaveCount(1);
// click on login button text
await page.locator("text=Login").click();
// expect to be redirected to login page with query parameter callbackUrl
await expect(page).toHaveURL(/\/auth\/login\?callbackUrl=.*/);
await user.apiLogin();
// manual redirect to booking page
await page.goto(`/booking/${booking.uid}`);
// expect login button to don't be in the page
await expect(page.locator("text=Login")).toHaveCount(0);
// reschedule-link click
await page.locator('[data-testid="reschedule-link"]').click();
await selectFirstAvailableTimeSlotNextMonth(page);
// data displayed in form should be user owner
const nameElement = await page.locator("input[name=name]");
const name = await nameElement.inputValue();
expect(name).toBe(user.name);
//same for email
const emailElement = await page.locator("input[name=email]");
const email = await emailElement.inputValue();
expect(email).toBe(user.email);
// reason to reschedule input should be visible textfield with name rescheduleReason
const reasonElement = await page.locator("textarea[name=rescheduleReason]");
await expect(reasonElement).toBeVisible();
// expect to be redirected to reschedule page
await confirmReschedule(page);
// should wait for URL but that path starts with booking/
await page.waitForURL(/\/booking\/.*/);
await expect(page).toHaveURL(/\/booking\/.*/);
const updatedBooking = await prisma.booking.findFirst({
where: { id: booking.id },
});
expect(updatedBooking).not.toBeNull();
expect(getBooking?.startTime).not.toBe(updatedBooking?.startTime);
expect(getBooking?.endTime).not.toBe(updatedBooking?.endTime);
expect(updatedBooking?.status).toBe(BookingStatus.ACCEPTED);
});
test("Owner shouldn't be able to reschedule when going directly to booking/rescheduleUid", async ({
page,
bookings,
users,
}) => {
const { booking, user } = 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 getBooking = await booking.self();
await page.goto(`/${user.username}/seats?rescheduleUid=${getBooking?.uid}&bookingUid=null`);
await selectFirstAvailableTimeSlotNextMonth(page);
// expect textarea with name notes to be visible
const notesElement = await page.locator("textarea[name=notes]");
await expect(notesElement).toBeVisible();
// expect button confirm instead of reschedule
await expect(page.locator('[data-testid="confirm-book-button"]')).toHaveCount(1);
// now login and try again
await user.apiLogin();
await page.goto(`/${user.username}/seats?rescheduleUid=${getBooking?.uid}&bookingUid=null`);
await selectFirstAvailableTimeSlotNextMonth(page);
await expect(page).toHaveTitle(/(?!.*reschedule).*/);
// expect button reschedule
await expect(page.locator('[data-testid="confirm-reschedule-button"]')).toHaveCount(1);
});
// @TODO: force 404 when rescheduleUid is not found
});