/** * Phase 3 mobile — long-press gesture. * * The `longpress` action attaches to `
` in FeedListCard and to * grid cells in FeedGrid. Holding for ≥ 500 ms fires `onlongpress`, * which opens the ContextSheet bottom sheet via the feed page's * `contextTarget` state. * * Setup needs at least one upload to render a card. We post via the * Phase 2 upload-client helper, then drive the gesture with the touch * helper. */ import { test, expect } from '../../fixtures/test'; import { uploadRaw, JPEG_MAGIC } from '../../helpers/upload-client'; import { longPress } from '../../helpers/touch'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; const SAMPLE_JPG = join(process.cwd(), 'fixtures', 'media', 'sample.jpg'); async function seedUpload(token: string, caption = 'Longpress fixture') { const body = readFileSync(SAMPLE_JPG); const res = await uploadRaw(token, body, { filename: 'lp.jpg', contentType: 'image/jpeg', caption, }); if (res.status !== 201) { const text = await res.text(); throw new Error(`Upload seed failed (${res.status}): ${text}`); } return res.json() as Promise<{ id: string }>; } test.describe('Mobile — long-press gesture', () => { test('long-press on a FeedListCard opens the ContextSheet', async ({ page, guest, signIn }) => { const g = await guest('Lp1'); await seedUpload(g.jwt, 'Hold to open'); await signIn(page, g); await page.goto('/feed'); // Wait for at least one article to render. Caption text appears once feed loads. const card = page.locator('article').filter({ hasText: g.displayName }).first(); await expect(card).toBeVisible({ timeout: 10_000 }); await longPress(page, card, 600); // The ContextSheet renders a dialog with role="dialog" + aria-modal="true". // Multiple sheets (UploadSheet, ContextSheet) may be in the DOM — match the // one that actually has aria-modal=true (i.e. the open one). // ContextSheet is always mounted (it just translates off-screen when closed). // Match the OPEN state by the `translate-y-0` class the component applies // when `open === true`. const sheet = page.locator('[role="dialog"][aria-modal="true"].translate-y-0'); await expect(sheet).toBeVisible({ timeout: 2_000 }); await expect(sheet.getByRole('button', { name: /abbrechen/i })).toBeVisible(); }); test('a quick tap (< 500 ms) does NOT open the ContextSheet — only opens the lightbox', async ({ page, guest, signIn }) => { const g = await guest('Lp2'); await seedUpload(g.jwt, 'Quick tap'); await signIn(page, g); await page.goto('/feed'); const card = page.locator('article').filter({ hasText: g.displayName }).first(); await expect(card).toBeVisible({ timeout: 10_000 }); // Simulate a short press (200 ms — well under the 500 ms threshold). await longPress(page, card, 200); // Within 1 s, no aria-modal=true dialog should be open (the ContextSheet // is "open" only when its aria-modal flag is true). // The ContextSheet stays mounted but `translate-y-0` is only set when open. await expect(page.locator('[role="dialog"][aria-modal="true"].translate-y-0')).toHaveCount(0, { timeout: 1_000 }); }); test('long-press suppresses the click that lands at pointerup (no double-open of lightbox)', async ({ page, guest, signIn }) => { const g = await guest('Lp3'); await seedUpload(g.jwt, 'Suppress click'); await signIn(page, g); await page.goto('/feed'); const card = page.locator('article').filter({ hasText: g.displayName }).first(); await expect(card).toBeVisible({ timeout: 10_000 }); await longPress(page, card, 700); // The longpress action sets `suppressNextClick = true` — so the lightbox // (separate role=dialog) should NOT appear in addition to the context sheet. // Exactly one aria-modal=true dialog should be open: the context sheet. await expect(page.locator('[role="dialog"][aria-modal="true"]')).toHaveCount(1, { timeout: 2_000 }); }); });