diff --git a/frontend/e2e/page-context-menu.spec.ts b/frontend/e2e/page-context-menu.spec.ts index d7ea12c..f42255c 100644 --- a/frontend/e2e/page-context-menu.spec.ts +++ b/frontend/e2e/page-context-menu.spec.ts @@ -373,5 +373,12 @@ test.describe('page action sheet (mobile long-press)', () => { await expect(page.getByTestId('page-action-add-tag')).toBeVisible(); await expect(page.getByTestId('page-action-save-image')).toBeVisible(); await expect(page.getByTestId('page-action-copy-link')).toBeVisible(); + + // The editor itself opens as a bottom Sheet on mobile (not a centered + // Modal), so the action-sheet → editor flow stays one consistent + // idiom. Sheet renders a `${testid}-scrim`; Modal a `${testid}-backdrop`. + await page.getByTestId('page-action-add-tag').click(); + await expect(page.getByTestId('add-tags-modal-scrim')).toBeVisible(); + await expect(page.getByTestId('add-tags-modal-backdrop')).toHaveCount(0); }); }); diff --git a/frontend/src/lib/components/AdaptiveDialog.svelte b/frontend/src/lib/components/AdaptiveDialog.svelte new file mode 100644 index 0000000..9f27e45 --- /dev/null +++ b/frontend/src/lib/components/AdaptiveDialog.svelte @@ -0,0 +1,53 @@ + + +{#if isMobile} + + {@render children()} + +{:else} + + {@render children()} + +{/if} diff --git a/frontend/src/lib/components/AdaptiveDialog.svelte.test.ts b/frontend/src/lib/components/AdaptiveDialog.svelte.test.ts new file mode 100644 index 0000000..f2dae93 --- /dev/null +++ b/frontend/src/lib/components/AdaptiveDialog.svelte.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/svelte'; +import { createRawSnippet } from 'svelte'; +import AdaptiveDialog from './AdaptiveDialog.svelte'; + +afterEach(() => { + cleanup(); + // @ts-expect-error — restore between tests + delete window.matchMedia; +}); + +// jsdom has no matchMedia; stub it to drive the breakpoint branch. +function stubMatchMedia(matches: boolean) { + window.matchMedia = vi.fn().mockImplementation((media: string) => ({ + matches, + media, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn() + })) as unknown as typeof window.matchMedia; +} + +const body = createRawSnippet(() => ({ + render: () => `

hello

` +})); + +describe('AdaptiveDialog', () => { + it('renders a centered Modal on desktop (the scrim is the Modal backdrop)', async () => { + stubMatchMedia(false); + render(AdaptiveDialog, { + props: { open: true, title: 'Tag this page', onClose: () => {}, testid: 'dlg', children: body } + }); + // Modal exposes a `${testid}-backdrop`; Sheet exposes `${testid}-scrim`. + expect(await screen.findByTestId('dlg-backdrop')).toBeTruthy(); + expect(screen.queryByTestId('dlg-scrim')).toBeNull(); + expect(screen.getByRole('dialog', { name: 'Tag this page' })).toBeTruthy(); + expect(screen.getByTestId('dlg-body')).toBeTruthy(); + }); + + it('renders a bottom Sheet on mobile', async () => { + stubMatchMedia(true); + render(AdaptiveDialog, { + props: { open: true, title: 'Tag this page', onClose: () => {}, testid: 'dlg', children: body } + }); + // The effect flips to mobile after mount; poll for the Sheet scrim. + expect(await screen.findByTestId('dlg-scrim')).toBeTruthy(); + expect(screen.queryByTestId('dlg-backdrop')).toBeNull(); + expect(screen.getByRole('dialog', { name: 'Tag this page' })).toBeTruthy(); + }); + + it('renders nothing when closed', () => { + stubMatchMedia(false); + render(AdaptiveDialog, { + props: { open: false, title: 'Tag this page', onClose: () => {}, testid: 'dlg', children: body } + }); + expect(screen.queryByRole('dialog')).toBeNull(); + }); +}); diff --git a/frontend/src/lib/components/AddToCollectionModal.svelte b/frontend/src/lib/components/AddToCollectionModal.svelte index 37f9499..d1d8d04 100644 --- a/frontend/src/lib/components/AddToCollectionModal.svelte +++ b/frontend/src/lib/components/AddToCollectionModal.svelte @@ -1,5 +1,5 @@ - + {#if loading}

Loading your collections…

{:else if error} @@ -235,7 +235,7 @@ {creating ? 'Creating…' : 'Create + add'} -
+