From c361b9bbc51c32f2c773302769a2218650847b56 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Wed, 17 Dec 2025 11:09:09 +0100 Subject: [PATCH] chore: enable booking redesign for e2e (#25642) * enable bookings-v3 feature flag for e2e tests WIP * fix e2e * update comments --- apps/web/playwright/booking-seats.e2e.ts | 18 +++-- apps/web/playwright/bookings-list.e2e.ts | 36 +++------ apps/web/playwright/fixtures/users.ts | 98 +++++++++++++++++++++++- 3 files changed, 118 insertions(+), 34 deletions(-) diff --git a/apps/web/playwright/booking-seats.e2e.ts b/apps/web/playwright/booking-seats.e2e.ts index 37bbaf6e45..d4cf1a3ed5 100644 --- a/apps/web/playwright/booking-seats.e2e.ts +++ b/apps/web/playwright/booking-seats.e2e.ts @@ -555,17 +555,23 @@ test.describe("Reschedule for booking with seats", () => { await page.waitForSelector('[data-testid="bookings"]'); await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click(); - await page.waitForTimeout(2000); - const href = await page.locator('[data-testid="reschedule"]').getAttribute("href"); + + // Wait for the reschedule link to be visible and have an href attribute + const rescheduleLink = page.locator('[data-testid="reschedule"]'); + await expect(rescheduleLink).toBeVisible(); + await expect(rescheduleLink).toHaveAttribute("href", /.+/); + + const href = await rescheduleLink.getAttribute("href"); const url = new URL(href!, page.url()); - const seatReferenceUid = url.searchParams.get('seatReferenceUid'); - if(!seatReferenceUid) { + const seatReferenceUid = url.searchParams.get("seatReferenceUid"); + if (!seatReferenceUid) { await page.reload(); await page.waitForSelector('[data-testid="bookings"]'); await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click(); - await page.waitForTimeout(2000); + await expect(rescheduleLink).toBeVisible(); + await expect(rescheduleLink).toHaveAttribute("href", /.+/); } - await page.locator('[data-testid="reschedule"]').click(); + await rescheduleLink.click(); await expect(page.getByText("Seats available").first()).toBeVisible(); await page.waitForURL((url) => { diff --git a/apps/web/playwright/bookings-list.e2e.ts b/apps/web/playwright/bookings-list.e2e.ts index e03ab8ef6b..2a8c3b7c65 100644 --- a/apps/web/playwright/bookings-list.e2e.ts +++ b/apps/web/playwright/bookings-list.e2e.ts @@ -58,16 +58,14 @@ test.describe("Bookings", () => { const firstUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(0); const secondUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(1); await expect( - firstUpcomingBooking.locator(`text=${bookingWhereFirstUserIsAttendee!.title}`) ).toBeVisible(); await expect( - secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`) ).toBeVisible(); }); - test("Cannot choose date range presets", async ({ page, users, bookings, webhooks }) => { + test("Cannot choose date range presets", async ({ page, users }) => { const firstUser = await users.create(); await firstUser.apiLogin(); const bookingsGetResponse = page.waitForResponse((response) => @@ -91,7 +89,7 @@ test.describe("Bookings", () => { test.describe("Past bookings", () => { test("Mark first guest as no-show", async ({ page, users, bookings, webhooks }) => { const firstUser = await users.create(); - const secondUser = await users.create(); + await users.create(); const bookingWhereFirstUserIsOrganizerFixture = await createBooking({ title: "Booking as organizer", @@ -112,7 +110,6 @@ test.describe("Bookings", () => { await page.goto(`/bookings/past`); const pastBookings = page.locator('[data-testid="past-bookings"]'); const firstPastBooking = pastBookings.locator('[data-testid="booking-item"]').nth(0); - const titleAndAttendees = firstPastBooking.locator('[data-testid="title-and-attendees"]'); const firstGuest = firstPastBooking.locator('[data-testid="guest"]').nth(0); await firstGuest.click(); await expect(page.locator('[data-testid="unmark-no-show"]')).toBeHidden(); @@ -142,7 +139,7 @@ test.describe("Bookings", () => { }); test("Mark 3rd attendee as no-show", async ({ page, users, bookings }) => { const firstUser = await users.create(); - const secondUser = await users.create(); + await users.create(); const bookingWhereFirstUserIsOrganizerFixture = await createBooking({ title: "Booking as organizer", @@ -158,23 +155,22 @@ test.describe("Bookings", () => { { name: "Fourth", email: "fourth@cal.com", timeZone: "Europe/Berlin" }, ], }); - const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self(); + await bookingWhereFirstUserIsOrganizerFixture.self(); await firstUser.apiLogin(); await page.goto(`/bookings/past`); const pastBookings = page.locator('[data-testid="past-bookings"]'); const firstPastBooking = pastBookings.locator('[data-testid="booking-item"]').nth(0); - const titleAndAttendees = firstPastBooking.locator('[data-testid="title-and-attendees"]'); const moreGuests = firstPastBooking.locator('[data-testid="more-guests"]'); await moreGuests.click(); const firstGuestInMore = page.getByRole("menuitemcheckbox").nth(0); - await expect(firstGuestInMore).toBeChecked({ checked: false }); + await expect(firstGuestInMore).toHaveAttribute("data-state", "unchecked"); await firstGuestInMore.click(); - await expect(firstGuestInMore).toBeChecked({ checked: true }); + await expect(firstGuestInMore).toHaveAttribute("data-state", "checked"); const updateNoShow = firstPastBooking.locator('[data-testid="update-no-show"]'); await updateNoShow.click(); await moreGuests.click(); - await expect(firstGuestInMore).toBeChecked({ checked: true }); + await expect(firstGuestInMore).toHaveAttribute("data-state", "checked"); }); test("Team admin/owner can mark first attendee as no-show", async ({ page, @@ -199,11 +195,10 @@ test.describe("Bookings", () => { }); const booking = await bookingFixture.self(); await adminUser.apiLogin(); - const { webhookReceiver, teamId } = await webhooks.createTeamReceiver(); + const { webhookReceiver } = await webhooks.createTeamReceiver(); await page.goto(`/bookings/past`); const pastBookings = page.locator('[data-testid="past-bookings"]'); const firstPastBooking = pastBookings.locator('[data-testid="booking-item"]').nth(0); - const titleAndAttendees = firstPastBooking.locator('[data-testid="title-and-attendees"]'); const firstGuest = firstPastBooking.locator('[data-testid="guest"]').nth(0); await firstGuest.click(); await expect(page.locator('[data-testid="mark-no-show"]')).toBeVisible(); @@ -232,7 +227,7 @@ test.describe("Bookings", () => { webhookReceiver.close(); }); - test("Can choose date range presets", async ({ page, users, bookings, webhooks }) => { + test("Can choose date range presets", async ({ page, users }) => { const firstUser = await users.create(); await firstUser.apiLogin(); const bookingsGetResponse = page.waitForResponse((response) => @@ -374,22 +369,15 @@ test.describe("Bookings", () => { //verify with the booking titles const firstUpcomingBooking = bookingListItems.nth(0); - await expect( - - firstUpcomingBooking.locator(`text=${thirdUserOrganizerBooking!.title}`) - ).toBeVisible(); + await expect(firstUpcomingBooking.locator(`text=${thirdUserOrganizerBooking!.title}`)).toBeVisible(); const secondUpcomingBooking = bookingListItems.nth(1); await expect( - secondUpcomingBooking.locator(`text=${thirdUserAttendeeIndividualBooking!.title}`) ).toBeVisible(); const thirdUpcomingBooking = bookingListItems.nth(2); - await expect( - - thirdUpcomingBooking.locator(`text=${thirdUserAttendeeTeamEvent!.title}`) - ).toBeVisible(); + await expect(thirdUpcomingBooking.locator(`text=${thirdUserAttendeeTeamEvent!.title}`)).toBeVisible(); }); test("Does not show booking from another user from collective event type when a member is filtered", async ({ @@ -414,7 +402,7 @@ test.describe("Bookings", () => { const { team } = await owner.getFirstTeamMembership(); const eventType = await owner.getFirstTeamEvent(team.id); - const { id: eventTypeId, title: teamEventTitle, slug: teamEventSlug } = eventType; + const { id: eventTypeId } = eventType; // remove myself from host of this event type await prisma.host.delete({ diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 1c9716ddd9..bf4a219d46 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -6,6 +6,7 @@ import { v4 } from "uuid"; import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes"; import stripe from "@calcom/features/ee/payments/server/stripe"; +import type { AppFlags } from "@calcom/features/flags/config"; import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -27,6 +28,18 @@ export function hashPassword(password: string) { return hashedPassword; } +/** + * Default feature flags enabled for all teams and organizations created in E2E tests. + * These flags represent the most common production features that should be tested by default. + */ +const DEFAULT_TEAM_FEATURE_FLAGS: Array = []; + +/** + * Default feature flags enabled for individual users created in E2E tests. + * Empty by default - users don't typically have feature flags unless explicitly needed. + */ +const DEFAULT_USER_FEATURE_FLAGS: Array = ["bookings-v3"]; + type UserFixture = ReturnType; export type CreateUsersFixture = ReturnType; @@ -155,6 +168,7 @@ const createTeamAndAddUser = async ( schedulingType, assignAllTeamMembersForSubTeamEvents, teamSlug, + teamFeatureFlags, }: { user: { id: number; email: string; username: string | null; role?: MembershipRole }; isUnpublished?: boolean; @@ -168,6 +182,7 @@ const createTeamAndAddUser = async ( schedulingType?: SchedulingType; assignAllTeamMembersForSubTeamEvents?: boolean; teamSlug?: string; + teamFeatureFlags?: Array; }, workerInfo: WorkerInfo ) => { @@ -196,13 +211,14 @@ const createTeamAndAddUser = async ( data.slug = !isUnpublished ? slug : undefined; if (isOrg && hasSubteam) { - const team = await createTeamAndAddUser({ user }, workerInfo); - await createTeamEventType(user, team, { + const subteam = await createTeamAndAddUser({ user, teamFeatureFlags }, workerInfo); + + await createTeamEventType(user, subteam, { schedulingType: schedulingType, assignAllTeamMembers: assignAllTeamMembersForSubTeamEvents, }); - await createTeamWorkflow(user, team); - data.children = { connect: [{ id: team.id }] }; + await createTeamWorkflow(user, subteam); + data.children = { connect: [{ id: subteam.id }] }; } data.orgProfiles = isOrg ? { @@ -235,6 +251,18 @@ const createTeamAndAddUser = async ( }, }); + // Enable feature flags for the team if specified + if (teamFeatureFlags && teamFeatureFlags.length > 0) { + await prisma.teamFeatures.createMany({ + data: teamFeatureFlags.map((featureFlag) => ({ + teamId: team.id, + featureId: featureFlag, + assignedBy: "e2e-fixture", + assignedAt: new Date(), + })), + }); + } + return team; }; @@ -270,6 +298,23 @@ export const createUsersFixture = ( | (CustomUserOpts & { organizationId?: number | null; overrideDefaultEventTypes?: boolean; + /** + * Feature flags to enable for this individual user. + * Defaults to DEFAULT_USER_FEATURE_FLAGS. + * Pass specific flags to enable user-level features. + * @default DEFAULT_USER_FEATURE_FLAGS + * @example + * ```typescript + * // Default feature flags + * const user = await users.create(); + * + * // Specific feature flags + * const user = await users.create({ + * userFeatureFlags: ["bookings-v3"] + * }); + * ``` + */ + userFeatureFlags?: Array; }) | null, scenario: { @@ -295,6 +340,31 @@ export const createUsersFixture = ( orgRequestedSlug?: string; assignAllTeamMembers?: boolean; assignAllTeamMembersForSubTeamEvents?: boolean; + /** + * Feature flags to enable for the created team(s) and organization(s). + * Defaults to DEFAULT_TEAM_FEATURE_FLAGS when hasTeam is true. + * Pass an empty array to disable default flags, or pass specific flags to override. + * Applies to both regular teams and organizations (when isOrg: true). + * @default DEFAULT_TEAM_FEATURE_FLAGS + * @example + * ```typescript + * // Default feature flags + * const user = await users.create({}, { hasTeam: true }); + * + * // Specific feature flags + * const user = await users.create({}, { + * hasTeam: true, + * teamFeatureFlags: ["pbac"] + * }); + * + * // Organizations also get the flags by default + * const user = await users.create({}, { + * hasTeam: true, + * isOrg: true + * }); // Org gets DEFAULT_TEAM_FEATURE_FLAGS + * ``` + */ + teamFeatureFlags?: Array; } = {} ) => { const _user = await prisma.user.create({ @@ -347,9 +417,27 @@ export const createUsersFixture = ( where: { id: _user.id }, include: userIncludes, }); + + // Enable feature flags for the user if specified + // Default to DEFAULT_USER_FEATURE_FLAGS if not specified + const userFeatureFlags = opts?.userFeatureFlags ?? DEFAULT_USER_FEATURE_FLAGS; + if (userFeatureFlags.length > 0) { + await prisma.userFeatures.createMany({ + data: userFeatureFlags.map((featureFlag) => ({ + userId: user.id, + featureId: featureFlag, + assignedBy: "e2e-fixture", + assignedAt: new Date(), + })), + }); + } + if (scenario.hasTeam) { const numberOfTeams = scenario.numberOfTeams || 1; for (let i = 0; i < numberOfTeams; i++) { + // Determine feature flags to use + const featureFlags = scenario.teamFeatureFlags ?? DEFAULT_TEAM_FEATURE_FLAGS; + const team = await createTeamAndAddUser( { user: { @@ -368,10 +456,12 @@ export const createUsersFixture = ( schedulingType: scenario.schedulingType, assignAllTeamMembersForSubTeamEvents: scenario.assignAllTeamMembersForSubTeamEvents, teamSlug: scenario?.teamSlug, + teamFeatureFlags: featureFlags, }, workerInfo ); store.teams.push(team); + const teamEvent = await createTeamEventType(user, team, scenario); if (scenario.teammates) { // Create Teammate users