459 lines
16 KiB
TypeScript
459 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 {
|
|
createNewSeatedEventType,
|
|
selectFirstAvailableTimeSlotNextMonth,
|
|
createUserWithSeatedEventAndAttendees,
|
|
} 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 page.waitForLoadState("networkidle");
|
|
|
|
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 page.locator('[data-testid="confirm_cancel"]').click();
|
|
|
|
await page.waitForResponse((res) => res.url().includes("api/cancel") && res.status() === 200);
|
|
|
|
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 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.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,
|
|
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 page.locator('[data-testid="confirm-reschedule-button"]').click();
|
|
|
|
// should wait for URL but that path starts with booking/
|
|
await page.waitForURL(/\/booking\/.*/);
|
|
|
|
await expect(page).toHaveURL(/\/booking\/.*/);
|
|
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
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
|
|
});
|