diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 5abfb821cc..e58c7dc98a 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -147,6 +147,7 @@ function BookingListItem(booking: BookingItemProps) { ? [ { id: "confirm", + bookingId: booking.id, label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"), onClick: () => { bookingConfirm(true); diff --git a/apps/web/pages/video/[uid].tsx b/apps/web/pages/video/[uid].tsx index 49fd7e26eb..ce6057f2e3 100644 --- a/apps/web/pages/video/[uid].tsx +++ b/apps/web/pages/video/[uid].tsx @@ -8,6 +8,7 @@ import { useState, useEffect, useRef } from "react"; import dayjs from "@calcom/dayjs"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { getCalVideoReference } from "@calcom/features/get-cal-video-reference"; import classNames from "@calcom/lib/classNames"; import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants"; import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns"; @@ -332,15 +333,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { bookRef.meetingPassword = null; }); } - - const videoReferences = bookingObj.references.filter((reference) => reference.type.includes("_video")); - const latestVideoReference = videoReferences[videoReferences.length - 1]; + const videoReference = getCalVideoReference(bookingObj.references); return { props: { - meetingUrl: latestVideoReference.meetingUrl ?? "", - ...(typeof latestVideoReference.meetingPassword === "string" && { - meetingPassword: latestVideoReference.meetingPassword, + meetingUrl: videoReference.meetingUrl ?? "", + ...(typeof videoReference.meetingPassword === "string" && { + meetingPassword: videoReference.meetingPassword, }), booking: { ...bookingObj, diff --git a/apps/web/playwright/reschedule.e2e.ts b/apps/web/playwright/reschedule.e2e.ts index bdb1edda01..d36262b956 100644 --- a/apps/web/playwright/reschedule.e2e.ts +++ b/apps/web/playwright/reschedule.e2e.ts @@ -3,9 +3,10 @@ import { expect } from "@playwright/test"; import dayjs from "@calcom/dayjs"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; +import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { test } from "./lib/fixtures"; -import { selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; +import { selectFirstAvailableTimeSlotNextMonth, bookTimeSlot } from "./lib/testUtils"; const IS_STRIPE_ENABLED = !!( process.env.STRIPE_CLIENT_ID && @@ -270,4 +271,64 @@ test.describe("Reschedule Tests", async () => { await page.locator('[data-testid="confirm-reschedule-button"]').click(); await expect(page).toHaveURL(/.*booking/); }); + test("Should load Valid Cal video url after rescheduling Opt in events", async ({ + page, + users, + bookings, + }) => { + const user = await users.create(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const eventType = user.eventTypes.find((e) => e.slug === "opt-in")!; + + const confirmBooking = async (bookingId: number) => { + await user.apiLogin(); + await page.goto("/bookings/upcoming"); + const elem = await page.locator(`[data-bookingid="${bookingId}"][data-testid="confirm"]`); + await elem.click(); + await page.getByTestId("toast-success").waitFor(); + await user.logout(); + }; + + await page.goto(`/${user.username}/${eventType.slug}`); + await selectFirstAvailableTimeSlotNextMonth(page); + await bookTimeSlot(page); + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + + const pageUrl = new URL(page.url()); + const pathSegments = pageUrl.pathname.split("/"); + const bookingUID = pathSegments[pathSegments.length - 1]; + + const currentBooking = await prisma.booking.findFirst({ where: { uid: bookingUID } }); + expect(currentBooking).not.toBeUndefined(); + // eslint-disable-next-line playwright/no-conditional-in-test + if (currentBooking) { + await confirmBooking(currentBooking.id); + + await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${currentBooking.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: currentBooking.uid } }); + expect(newBooking).not.toBeUndefined(); + expect(newBooking?.status).toBe(BookingStatus.PENDING); + // eslint-disable-next-line playwright/no-conditional-in-test + if (newBooking) { + await confirmBooking(newBooking?.id); + + const booking = await prisma.booking.findFirst({ where: { id: newBooking.id } }); + expect(booking).not.toBeUndefined(); + expect(booking?.status).toBe(BookingStatus.ACCEPTED); + const locationVideoCallUrl = bookingMetadataSchema.parse(booking?.metadata || {})?.videoCallUrl; + expect(locationVideoCallUrl).not.toBeUndefined(); + + // eslint-disable-next-line playwright/no-conditional-in-test + if (booking && locationVideoCallUrl) { + await page.goto(locationVideoCallUrl); + await expect(page.frameLocator("iFrame").locator('text="Continue"')).toBeVisible(); + } + } + } + }); }); diff --git a/packages/features/get-cal-video-reference.test.ts b/packages/features/get-cal-video-reference.test.ts new file mode 100644 index 0000000000..037c4b4ae6 --- /dev/null +++ b/packages/features/get-cal-video-reference.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; + +import { getCalVideoReference } from "./get-cal-video-reference"; + +describe("Cal Video", () => { + it("should load latest cal video reference", () => { + expect( + getCalVideoReference([ + { + uid: "UID1", + type: "daily_video", + meetingUrl: "ID1", + meetingPassword: "P1", + }, + { + uid: "UID2", + type: "daily_video", + meetingUrl: "ID2", + meetingPassword: "P2", + }, + ]) + ).toEqual({ + uid: "UID2", + type: "daily_video", + meetingUrl: "ID2", + meetingPassword: "P2", + }); + }); +}); diff --git a/packages/features/get-cal-video-reference.ts b/packages/features/get-cal-video-reference.ts new file mode 100644 index 0000000000..d662e22456 --- /dev/null +++ b/packages/features/get-cal-video-reference.ts @@ -0,0 +1,12 @@ +export type Reference = { + uid: string; + type: string; + meetingUrl: string | null; + meetingPassword: string | null; +}; + +export const getCalVideoReference = (references: Reference[]) => { + const videoReferences = references.filter((reference) => reference.type.includes("_video")); + const latestVideoReference = videoReferences[videoReferences.length - 1]; + return latestVideoReference; +}; diff --git a/packages/ui/components/table/TableActions.tsx b/packages/ui/components/table/TableActions.tsx index 6cfecb8dec..3ae64c781d 100644 --- a/packages/ui/components/table/TableActions.tsx +++ b/packages/ui/components/table/TableActions.tsx @@ -21,6 +21,7 @@ export type ActionType = { label: string; disabled?: boolean; color?: ButtonBaseProps["color"]; + bookingId?: number; } & ( | { href: string; onClick?: never; actions?: never } | { href?: never; onClick: (e: React.MouseEvent) => void; actions?: never } @@ -61,6 +62,7 @@ export const DropdownActions = ({ data-testid={action.id} StartIcon={action.icon} href={action.href} + data-bookingid={action.bookingId} onClick={action.onClick || defaultAction}> {action.label} @@ -87,6 +89,7 @@ export const TableActions: FC = ({ actions }) => { StartIcon={action.icon} {...(action?.actions ? { EndIcon: ChevronDown } : null)} disabled={action.disabled} + data-bookingid={action.bookingId} color={action.color || "secondary"}> {action.label}