import { expect, test, type Page } from "@playwright/test"; import { stripePaymentDialogStyles } from "@/app/_components/payment/stripe-payment-dialog.styles"; import { mockCoreApis } from "@e2e/fixtures/api-mocks"; const viewports = [ { width: 390, height: 844 }, { width: 405, height: 797 }, { width: 540, height: 900 }, ] as const; test.beforeEach(async ({ page }) => { await mockCoreApis(page); }); for (const viewport of viewports) { test(`payment portal stays viewport-bound at ${viewport.width}x${viewport.height}`, async ({ page, }) => { await page.setViewportSize(viewport); await page.goto("/subscription?type=topup", { waitUntil: "domcontentloaded", }); await mountPaymentPanel(page, 180); await expectViewportScrim(page, viewport); await expectCenteredShortPanel(page, viewport); await mountPaymentPanel(page, 1_200); await expectViewportScrim(page, viewport); await expectScrollableTallPanel(page, viewport); }); } async function mountPaymentPanel(page: Page, contentHeight: number) { await page.evaluate( ({ dialogClassName, overlayClassName, requestedContentHeight }) => { document.querySelector('[data-testid="payment-layout-overlay"]')?.remove(); document.querySelector('[data-testid="transformed-checkout-host"]')?.remove(); const transformedCheckoutHost = document.createElement("div"); transformedCheckoutHost.dataset.testid = "transformed-checkout-host"; transformedCheckoutHost.style.transform = "translateX(50%)"; document.body.appendChild(transformedCheckoutHost); const overlay = document.createElement("div"); overlay.dataset.testid = "payment-layout-overlay"; overlay.className = overlayClassName; const panel = document.createElement("div"); panel.dataset.testid = "payment-layout-panel"; panel.className = dialogClassName; const content = document.createElement("div"); content.style.height = `${requestedContentHeight}px`; content.textContent = "Complete payment"; panel.appendChild(content); overlay.appendChild(panel); // Mirrors ModalPortal: the overlay must be a body child, never a child of // the transformed checkout host that triggered it. document.body.appendChild(overlay); }, { dialogClassName: stripePaymentDialogStyles.dialog, overlayClassName: stripePaymentDialogStyles.overlay, requestedContentHeight: contentHeight, }, ); } async function expectViewportScrim( page: Page, viewport: { width: number; height: number }, ) { const overlay = page.getByTestId("payment-layout-overlay"); await expect(overlay).toBeVisible(); const box = await overlay.boundingBox(); expect(box).not.toBeNull(); expect(box?.x).toBeCloseTo(0, 0); expect(box?.y).toBeCloseTo(0, 0); expect(box?.width).toBeCloseTo(viewport.width, 0); expect(box?.height).toBeCloseTo(viewport.height, 0); await expect(overlay.evaluate((node) => node.parentElement === document.body)) .resolves.toBe(true); } async function expectCenteredShortPanel( page: Page, viewport: { width: number; height: number }, ) { const panel = page.getByTestId("payment-layout-panel"); const box = await panel.boundingBox(); expect(box).not.toBeNull(); expect(box?.y ?? 0).toBeGreaterThan(100); expect(Math.abs((box?.y ?? 0) * 2 + (box?.height ?? 0) - viewport.height)) .toBeLessThanOrEqual(2); } async function expectScrollableTallPanel( page: Page, viewport: { width: number; height: number }, ) { const panel = page.getByTestId("payment-layout-panel"); const box = await panel.boundingBox(); const scrollMetrics = await panel.evaluate((node) => ({ clientHeight: node.clientHeight, scrollHeight: node.scrollHeight, })); expect(box).not.toBeNull(); expect(box?.y ?? 0).toBeGreaterThanOrEqual(15); expect(box?.y ?? 100).toBeLessThanOrEqual(25); expect((box?.y ?? 0) + (box?.height ?? 0)) .toBeLessThanOrEqual(viewport.height - 15); expect(scrollMetrics.scrollHeight).toBeGreaterThan(scrollMetrics.clientHeight); }