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:
Rob Jackson
2023-06-10 03:47:50 +00:00
committed by GitHub
co-authored by Efraín Rochín
parent bb31c4ba14
commit 0d01d3246a
11 changed files with 151 additions and 30 deletions
@@ -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);
+4 -5
View File
@@ -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);
+104 -15
View File
@@ -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();
});
});
+6 -1
View File
@@ -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 } }),
};
};
+1
View File
@@ -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);
+2 -2
View File
@@ -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) {
+19
View File
@@ -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,