test: Add E2E tests for seated attendee rescheduling or cancelling bookings (#9422)
* Add test for attendees rescheduling bookings * Add tests for rescheduling seated bookings (first attendee) * Fix tests not skipping disabled times on new booker UI * Add tests for rescheduling seated bookings (all attendees) * Remove duplicate tests (my mistake) * Add explicit test ids for cancel inputs * Add E2E tests for seated attendees cancelling their bookings * Update E2E tests for a seated attendee rescheduling their booking * Update tests to remove dependence on localised text * Updated seated E2E tests to remove testing of unused URL --------- Co-authored-by: Efraín Rochín <roae.85@gmail.com>
This commit is contained in:
co-authored by
Efraín Rochín
parent
bb31c4ba14
commit
0d01d3246a
@@ -161,7 +161,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||
)}
|
||||
onClick={() => reserveSlot(slot)}
|
||||
data-testid="time">
|
||||
data-testid="time"
|
||||
data-disabled="false">
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && (
|
||||
<p
|
||||
|
||||
@@ -59,6 +59,7 @@ export default function CancelBooking(props: Props) {
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
||||
<TextArea
|
||||
data-testid="cancel_reason"
|
||||
ref={cancelBookingRef}
|
||||
placeholder={t("cancellation_reason_placeholder")}
|
||||
value={cancellationReason}
|
||||
@@ -75,7 +76,7 @@ export default function CancelBooking(props: Props) {
|
||||
{t("nevermind")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
data-testid="confirm_cancel"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
@@ -109,15 +109,14 @@ testBothBookers.describe("pro user", () => {
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.locator('[data-testid="cancel"]').first().click();
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.startsWith("/booking/");
|
||||
});
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
|
||||
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
|
||||
|
||||
expect(cancelledHeadline).toBe("This event is cancelled");
|
||||
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
|
||||
await expect(cancelledHeadline).toBeVisible();
|
||||
|
||||
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -21,7 +22,14 @@ 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 },
|
||||
{
|
||||
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
|
||||
@@ -105,21 +113,13 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Make E2E test: Attendee #1 should be able to cancel his booking
|
||||
// todo("Attendee #1 should be able to cancel his booking");
|
||||
// TODO: Make E2E test: Attendee #1 should be able to reschedule his booking
|
||||
// todo("Attendee #1 should be able to reschedule his booking");
|
||||
// TODO: Make E2E test: All attendees canceling should delete the booking for the User
|
||||
// todo("All attendees canceling should delete the booking for the User");
|
||||
});
|
||||
|
||||
testBothBookers.describe("Reschedule for booking with seats", () => {
|
||||
test("Should reschedule booking with seats", async ({ page, users, bookings }) => {
|
||||
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: {
|
||||
@@ -137,6 +137,81 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
||||
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 },
|
||||
});
|
||||
@@ -156,6 +231,20 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
||||
|
||||
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 },
|
||||
@@ -260,7 +349,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
||||
// Now we cancel the booking as the organizer
|
||||
await page.goto(`/booking/${booking.uid}?cancel=true`);
|
||||
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
@@ -309,7 +398,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
||||
`/booking/${references[0].referenceUid}?cancel=true&seatReferenceUid=${references[0].referenceUid}`
|
||||
);
|
||||
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
|
||||
const oldBooking = await prisma.booking.findFirst({
|
||||
where: { uid: booking.uid },
|
||||
@@ -364,7 +453,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
||||
`/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[0].referenceUid}`
|
||||
);
|
||||
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
@@ -375,7 +464,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
||||
);
|
||||
|
||||
// Page should not be 404
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
|
||||
@@ -47,14 +47,13 @@ test.skip("dynamic booking", async ({ page, users }) => {
|
||||
|
||||
await test.step("Can cancel the recently created booking", async () => {
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.locator('[data-testid="cancel"]').first().click();
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.startsWith("/booking");
|
||||
});
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
|
||||
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
|
||||
|
||||
expect(cancelledHeadline).toBe("This event is cancelled");
|
||||
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
|
||||
await expect(cancelledHeadline).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ export const createBookingsFixture = (page: Page) => {
|
||||
store.bookings.push(bookingFixture);
|
||||
return bookingFixture;
|
||||
},
|
||||
update: async (args: Prisma.BookingUpdateArgs) => await prisma.booking.update(args),
|
||||
get: () => store.bookings,
|
||||
delete: async (id: number) => {
|
||||
await prisma.booking.delete({
|
||||
@@ -80,7 +81,11 @@ const createBookingFixture = (booking: Booking, page: Page) => {
|
||||
return {
|
||||
id: store.booking.id,
|
||||
uid: store.booking.uid,
|
||||
self: async () => await prisma.booking.findUnique({ where: { id: store.booking.id } }),
|
||||
self: async () =>
|
||||
await prisma.booking.findUnique({
|
||||
where: { id: store.booking.id },
|
||||
include: { attendees: true, seatsReferences: true },
|
||||
}),
|
||||
delete: async () => await prisma.booking.delete({ where: { id: store.booking.id } }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -77,6 +77,7 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
|
||||
{ title: "30 min", slug: "30-min", length: 30 },
|
||||
{ title: "Paid", slug: "paid", length: 30, price: 1000 },
|
||||
{ title: "Opt in", slug: "opt-in", requiresConfirmation: true, length: 30 },
|
||||
{ title: "Seated", slug: "seated", seatsPerTimeSlot: 2, length: 30 },
|
||||
];
|
||||
|
||||
if (opts?.eventTypes) defaultEventTypes = defaultEventTypes.concat(opts.eventTypes);
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function selectFirstAvailableTimeSlotNextMonth(page: Page) {
|
||||
await page.waitForTimeout(1000);
|
||||
// TODO: Find out why the first day is always booked on tests
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
|
||||
await page.locator('[data-testid="time"]').nth(0).click();
|
||||
await page.locator('[data-testid="time"][data-disabled="false"]').nth(0).click();
|
||||
}
|
||||
|
||||
export async function selectSecondAvailableTimeSlotNextMonth(page: Page) {
|
||||
@@ -110,7 +110,7 @@ export async function selectSecondAvailableTimeSlotNextMonth(page: Page) {
|
||||
await page.waitForTimeout(1000);
|
||||
// TODO: Find out why the first day is always booked on tests
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
|
||||
await page.locator('[data-testid="time"]').nth(1).click();
|
||||
await page.locator('[data-testid="time"][data-disabled="false"]').nth(1).click();
|
||||
}
|
||||
|
||||
async function bookEventOnThisPage(page: Page) {
|
||||
|
||||
@@ -221,4 +221,23 @@ testBothBookers.describe("Reschedule Tests", async () => {
|
||||
expect(newBooking).not.toBeNull();
|
||||
expect(newBooking?.status).toBe(BookingStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
test("Attendee should be able to reschedule a booking", async ({ page, users, bookings }) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes[0];
|
||||
const booking = await bookings.create(user.id, user.username, eventType.id);
|
||||
|
||||
// Go to attendee's reschedule link
|
||||
await page.goto(`/reschedule/${booking.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page).toHaveURL(/.*booking/);
|
||||
|
||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
|
||||
expect(newBooking).not.toBeNull();
|
||||
expect(newBooking?.status).toBe(BookingStatus.ACCEPTED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,6 +83,7 @@ export const AvailableTimes = ({
|
||||
key={slot.time}
|
||||
disabled={bookingFull}
|
||||
data-testid="time"
|
||||
data-disabled={bookingFull}
|
||||
data-time={slot.time}
|
||||
onClick={() => onTimeSelect(slot.time)}
|
||||
className="min-h-9 mb-2 flex h-auto w-full flex-col justify-center py-2"
|
||||
|
||||
@@ -45,6 +45,12 @@ export const getCalEventResponses = ({
|
||||
if (!label) {
|
||||
throw new Error('Missing label for booking field "' + field.name + '"');
|
||||
}
|
||||
|
||||
// If the guests field is hidden (disableGuests is set on the event type) don't try and infer guests from attendees list
|
||||
if (field.name == "guests" && field.hidden) {
|
||||
backwardCompatibleResponses[field.name] = [];
|
||||
}
|
||||
|
||||
if (field.editable === "user" || field.editable === "user-readonly") {
|
||||
calEventUserFieldsResponses[field.name] = {
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user