Files
calendar/packages/embeds/embed-core/playwright/tests/two-step-slot-selection.e2e.ts
T
+2
Rajiv SahalGitHubrajiv@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 RichelsenHariom Balhara
8c39210a12 fix: scroll issues in Embed (#26583)
* 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>
2026-01-21 15:44:33 -03:00

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();
});
});