+2![Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)









Rajiv Sahal
GitHub
rajiv@cal.com <sahalrajiv6900@gmail.com>
rajiv@cal.com <sahalrajiv6900@gmail.com>
rajiv@cal.com <sahalrajiv6900@gmail.com>
rajiv@cal.com <sahalrajiv6900@gmail.com>
rajiv@cal.com <sahalrajiv6900@gmail.com>
rajiv@cal.com <sahalrajiv6900@gmail.com>
rajiv@cal.com <sahalrajiv6900@gmail.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Peer Richelsen
Hariom Balhara
8c39210a12
* chore: add new view for two step slot selection for embed * fix: make sure two step slot selection is false by default * chore: update embed playground to showcase two step slot selection * chore: add tests * chore: update embed snippet generator code to include two step slot selection * chore: update docs * fix: back button styling * chore: implement feedback from cubic * fix: add missing isEnableTwoStepSlotSelectionVisible properties to test mock store Co-Authored-By: unknown <> * chore: implement PR feedback * chore: update slot selection modal * chore: add prop to hide available times header * fix: scope two-step slot selection visibility to mobile only Restores the isMobile check in the timeslot visibility guard to ensure desktop embeds continue to show the booking UI when two-step slot selection is enabled. The feature should only hide the timeslot list on mobile devices. Addresses Cubic AI review feedback (confidence 9/10). Co-Authored-By: unknown <> * fixup: use translations for loading instead of actual word * fix: type check * fix: add window.matchMedia mock for jsdom test environment Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: add window.matchMedia mock to packages/testing/src/setupVitest.ts Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: unit tests failing * add a width style to the two-step slot selection embed container. * refactor: rename enableTwoStepSlotSelection to useSlotsViewOnSmallScreen - Rename external-facing embed config option from enableTwoStepSlotSelection to useSlotsViewOnSmallScreen - Update URL parameter parsing in embed-iframe.ts - Update type definitions in types.ts and index.d.ts - Update internal variable names to slotsViewOnSmallScreen for consistency - Update documentation, playground examples, and tests Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Add config to prefill all booking fields so that slot has confirm button along with it * chore: implement PR feedback * fix: move twoStepSlotSelection embed outside misc-embeds div to fix e2e test The e2e test was failing because the misc-embeds div content was intercepting pointer events on mobile viewport. This follows the same pattern used for skeletonDemo - the embed is now outside misc-embeds and the div is hidden when testing this namespace. Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: prevent pointer event interception in twoStepSlotSelection embed Hide heading and note elements and set pointer-events: none on container elements while keeping pointer-events: auto on the iframe container. This prevents the container from intercepting clicks on the iframe during mobile viewport e2e tests. Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: hide all non-essential content for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: disable pointer-events on body for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: remove pointer-events: auto on container to let clicks pass through to iframe Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: explicitly set pointer-events: none on container and inline-embed-container Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: also set pointer-events: none on html element to prevent interception Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: make .place div fill viewport for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: failing embed tests * fixup * fixup fixup * fix: failing tests * fix: update checks for verifying prefilled date --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
187 lines
7.6 KiB
TypeScript
187 lines
7.6 KiB
TypeScript
import { test } from "@calcom/web/playwright/lib/fixtures";
|
|
import { expect } from "@playwright/test";
|
|
import { deleteAllBookingsByEmail, ensureEmbedIframe, getBooking } from "../lib/testUtils";
|
|
|
|
// Mobile viewport dimensions
|
|
const MOBILE_VIEWPORT = { width: 375, height: 667 };
|
|
|
|
test.describe("Two Step Slot Selection", () => {
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
test.afterEach(async () => {
|
|
await deleteAllBookingsByEmail("john@booker.com");
|
|
});
|
|
|
|
test("should open slot selection modal on mobile when date is clicked", async ({ page, embeds }) => {
|
|
// Set mobile viewport
|
|
await page.setViewportSize(MOBILE_VIEWPORT);
|
|
|
|
const calNamespace = "twoStepSlotSelection";
|
|
await embeds.gotoPlayground({ calNamespace, url: "/?only=ns:twoStepSlotSelection" });
|
|
|
|
const embedIframe = await ensureEmbedIframe({ calNamespace, page, pathname: "/free/30min" });
|
|
|
|
// Wait for the booker to be ready
|
|
await embedIframe.waitForSelector('[data-testid="day"]');
|
|
|
|
// Click on an available date
|
|
await embedIframe.locator('[data-testid="day"][data-disabled="false"]').first().click();
|
|
|
|
// Verify the slot selection modal opens (it has the two-step-slot-selection-modal-header class)
|
|
await expect(embedIframe.locator(".two-step-slot-selection-modal-header")).toBeVisible();
|
|
|
|
// Verify time slots are visible in the modal
|
|
await expect(embedIframe.locator('[data-testid="time"]').first()).toBeVisible();
|
|
});
|
|
|
|
test("should complete booking with prefilled form data (skipConfirmStep) in two-step modal", async ({
|
|
page,
|
|
embeds,
|
|
}) => {
|
|
// Set mobile viewport
|
|
await page.setViewportSize(MOBILE_VIEWPORT);
|
|
|
|
const calNamespace = "twoStepSlotSelection";
|
|
await embeds.gotoPlayground({ calNamespace, url: "/?only=ns:twoStepSlotSelection" });
|
|
|
|
const embedIframe = await ensureEmbedIframe({ calNamespace, page, pathname: "/free/30min" });
|
|
|
|
// Wait for the booker to be ready
|
|
await embedIframe.waitForSelector('[data-testid="day"]');
|
|
|
|
// Go to next month to ensure availability
|
|
await embedIframe.click('[data-testid="incrementMonth"]');
|
|
await embedIframe.waitForTimeout(1000);
|
|
|
|
// Click on an available date
|
|
await embedIframe.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
|
|
|
|
// Wait for modal to open
|
|
await expect(embedIframe.locator(".two-step-slot-selection-modal-header")).toBeVisible();
|
|
|
|
// Wait for time slots to be visible in the modal
|
|
await embedIframe.waitForSelector('[data-testid="time"]');
|
|
|
|
// Wait for the async skipConfirmStep validation to complete
|
|
// The useSkipConfirmStep hook runs an async schema validation when bookerState becomes "selecting_time"
|
|
// We need to give it time to validate the prefilled form data
|
|
await embedIframe.waitForTimeout(1000);
|
|
|
|
// Click on a time slot - this should show the confirm button since form is prefilled
|
|
await embedIframe.locator('[data-testid="time"]').first().click();
|
|
|
|
// Wait for confirm button to appear (skip-confirm-book-button appears when skipConfirmStep is true)
|
|
await expect(embedIframe.locator('[data-testid="skip-confirm-book-button"]')).toBeVisible();
|
|
|
|
// Set up response listener before clicking
|
|
const responsePromise = page.waitForResponse("**/api/book/event");
|
|
|
|
// Click the confirm button
|
|
await embedIframe.locator('[data-testid="skip-confirm-book-button"]').click();
|
|
|
|
// Verify the button shows loading state (modal should stay open while loading)
|
|
// The modal should remain visible during booking
|
|
await expect(embedIframe.locator(".two-step-slot-selection-modal-header")).toBeVisible();
|
|
|
|
// Wait for booking response
|
|
const response = await responsePromise;
|
|
expect(response.status()).toBe(200);
|
|
|
|
const booking = (await response.json()) as { uid: string };
|
|
|
|
// Verify the booking was created with booker and prefilled guest
|
|
const bookingFromDb = await getBooking(booking.uid);
|
|
expect(bookingFromDb.attendees.length).toBe(2);
|
|
const attendeeEmails = bookingFromDb.attendees.map((a) => a.email);
|
|
expect(attendeeEmails).toContain("john@booker.com");
|
|
expect(attendeeEmails).toContain("guest@example.com");
|
|
});
|
|
|
|
test("should show confirm button next to slot when form is prefilled", async ({ page, embeds }) => {
|
|
// Set mobile viewport
|
|
await page.setViewportSize(MOBILE_VIEWPORT);
|
|
|
|
const calNamespace = "twoStepSlotSelection";
|
|
await embeds.gotoPlayground({ calNamespace, url: "/?only=ns:twoStepSlotSelection" });
|
|
|
|
const embedIframe = await ensureEmbedIframe({ calNamespace, page, pathname: "/free/30min" });
|
|
|
|
// Wait for the booker to be ready
|
|
await embedIframe.waitForSelector('[data-testid="day"]');
|
|
|
|
// Go to next month
|
|
await embedIframe.click('[data-testid="incrementMonth"]');
|
|
await embedIframe.waitForTimeout(1000);
|
|
|
|
// Click on an available date
|
|
await embedIframe.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
|
|
|
|
// Wait for modal to open
|
|
await expect(embedIframe.locator(".two-step-slot-selection-modal-header")).toBeVisible();
|
|
|
|
// Click on a time slot
|
|
await embedIframe.locator('[data-testid="time"]').first().click();
|
|
|
|
// Verify confirm button appears next to the slot
|
|
const confirmButton = embedIframe.locator('[data-testid="skip-confirm-book-button"]');
|
|
await expect(confirmButton).toBeVisible();
|
|
|
|
// Verify button text is "Confirm" or "Pay and Book"
|
|
const buttonText = await confirmButton.textContent();
|
|
expect(buttonText?.toLowerCase()).toMatch(/confirm|pay/);
|
|
});
|
|
|
|
test("should close modal when back button is clicked", async ({ page, embeds }) => {
|
|
// Set mobile viewport
|
|
await page.setViewportSize(MOBILE_VIEWPORT);
|
|
|
|
const calNamespace = "twoStepSlotSelection";
|
|
await embeds.gotoPlayground({ calNamespace, url: "/?only=ns:twoStepSlotSelection" });
|
|
|
|
const embedIframe = await ensureEmbedIframe({ calNamespace, page, pathname: "/free/30min" });
|
|
|
|
// Wait for the booker to be ready
|
|
await embedIframe.waitForSelector('[data-testid="day"]');
|
|
|
|
// Click on an available date
|
|
await embedIframe.locator('[data-testid="day"][data-disabled="false"]').first().click();
|
|
|
|
// Verify modal opens
|
|
await expect(embedIframe.locator(".two-step-slot-selection-modal-header")).toBeVisible();
|
|
|
|
// Click the back button in the modal header
|
|
await embedIframe.locator(".two-step-slot-selection-modal-header button").first().click();
|
|
|
|
// Verify modal closes
|
|
await expect(embedIframe.locator(".two-step-slot-selection-modal-header")).not.toBeVisible();
|
|
});
|
|
|
|
test("should NOT open modal on desktop even with useSlotsViewOnSmallScreen enabled", async ({
|
|
page,
|
|
embeds,
|
|
}) => {
|
|
// Use desktop viewport (default is usually larger than 768px)
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
|
|
|
const calNamespace = "twoStepSlotSelection";
|
|
await embeds.gotoPlayground({ calNamespace, url: "/?only=ns:twoStepSlotSelection" });
|
|
|
|
const embedIframe = await ensureEmbedIframe({ calNamespace, page, pathname: "/free/30min" });
|
|
|
|
// Wait for the booker to be ready
|
|
await embedIframe.waitForSelector('[data-testid="day"]');
|
|
|
|
// Click on an available date
|
|
await embedIframe.locator('[data-testid="day"][data-disabled="false"]').first().click();
|
|
|
|
// Wait a bit to ensure modal would have opened if it was going to
|
|
await embedIframe.waitForTimeout(500);
|
|
|
|
// Verify the modal does NOT open on desktop
|
|
await expect(embedIframe.locator(".two-step-slot-selection-modal-header")).not.toBeVisible();
|
|
|
|
// Time slots should be visible in the regular view (not modal)
|
|
await expect(embedIframe.locator('[data-testid="time"]').first()).toBeVisible();
|
|
});
|
|
});
|