diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.module.css b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.module.css new file mode 100644 index 000000000..64d64e6e4 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.module.css @@ -0,0 +1,3 @@ +.modal { + width: min(calc(100dvw - 48px), 1320px); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx index d0552f58c..fd63247f0 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx @@ -2,6 +2,7 @@ import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen'; import { SessionProfile } from '@/app/(main)/websites/[websiteId]/sessions/SessionProfile'; import { useNavigation } from '@/components/hooks'; +import styles from './SessionModal.module.css'; export interface SessionModalProps extends ModalProps { websiteId: string; @@ -25,12 +26,13 @@ export function SessionModal({ websiteId, ...props }: SessionModalProps) { - + {({ close }) => ( diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx index 7bcdf0802..fcd18fecd 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfileModal.tsx @@ -3,6 +3,7 @@ import { Column, Dialog, Modal } from '@umami/react-zen'; import { useRouter, useSearchParams } from 'next/navigation'; import { buildPath } from '@/lib/url'; import { SessionProfile } from './SessionProfile'; +import styles from './SessionModal.module.css'; export function SessionProfileModal({ websiteId, @@ -28,8 +29,15 @@ export function SessionProfileModal({ }; return ( - - + + diff --git a/tests/e2e/session-modal-dismiss.spec.ts b/tests/e2e/session-modal-dismiss.spec.ts new file mode 100644 index 000000000..ebb7dca16 --- /dev/null +++ b/tests/e2e/session-modal-dismiss.spec.ts @@ -0,0 +1,72 @@ +import { type APIRequestContext, expect, test } from '@playwright/test'; +import { uuid } from '../../src/lib/crypto'; +import { type Auth, authHeaders, deleteWebsite, loginPage, umamiUser } from './helpers'; + +// The session modal is a bottom sheet capped at 1320px and centered, so on a +// wide viewport there are dark side margins. Clicking those margins must +// dismiss the modal (regression test for the full-width click-catcher bug). +test.use({ viewport: { width: 1600, height: 900 } }); + +async function createWebsite(request: APIRequestContext, auth: Auth, websiteId: string) { + const response = await request.post('/api/websites', { + headers: authHeaders(auth), + data: { + id: websiteId, + createdBy: umamiUser.id, + name: 'Modal dismiss test', + domain: 'modal-dismiss-test.com', + }, + }); + expect(response.status()).toBe(200); +} + +test.describe('SessionModal outside-click dismissal', () => { + let auth: Auth; + const websiteId = uuid(); + + test.beforeEach(async ({ page, request }) => { + auth = await loginPage(page, request); + await createWebsite(request, auth, websiteId); + // The modal opens purely from the `session` query param, so a random id is + // enough to render it — no seeded session data required. + await page.goto(`/websites/${websiteId}/sessions?session=${uuid()}`); + await expect(page.getByRole('dialog')).toBeVisible(); + }); + + test.afterEach(async ({ request }) => { + await deleteWebsite(request, auth, websiteId); + }); + + test('clicking in the left side margin dismisses the modal', async ({ page }) => { + const box = await page.getByRole('dialog').boundingBox(); + const marginX = 20; + const midY = box.y + box.height / 2; + expect(marginX).toBeLessThan(box.x); // genuinely outside the sheet + + await page.mouse.click(marginX, midY); + + await expect(page.getByRole('dialog')).toBeHidden(); + await expect(page).not.toHaveURL(/session=/); + }); + + test('clicking in the right side margin dismisses the modal', async ({ page }) => { + const box = await page.getByRole('dialog').boundingBox(); + const marginX = page.viewportSize().width - 20; + const midY = box.y + box.height / 2; + expect(marginX).toBeGreaterThan(box.x + box.width); // genuinely outside the sheet + + await page.mouse.click(marginX, midY); + + await expect(page.getByRole('dialog')).toBeHidden(); + await expect(page).not.toHaveURL(/session=/); + }); + + test('control: clicking inside the sheet keeps the modal open', async ({ page }) => { + const box = await page.getByRole('dialog').boundingBox(); + + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); + + await expect(page.getByRole('dialog')).toBeVisible(); + await expect(page).toHaveURL(/session=/); + }); +});