/** * Critical a11y fix — the focusTrap action keeps Tab within open modals * and lets Escape dismiss them, then restores focus to the originating * element. This spec covers the LightboxModal, which is the most-used * focus-trap consumer in the app. */ import { test, expect } from '../../fixtures/test'; import { uploadRaw } from '../../helpers/upload-client'; 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): Promise<{ id: string }> { const res = await uploadRaw(token, readFileSync(SAMPLE_JPG), { filename: 'ft.jpg', contentType: 'image/jpeg', caption: 'Focus-trap fixture', }); if (res.status !== 201) throw new Error(`Upload seed failed (${res.status})`); return (await res.json()) as { id: string }; } test.describe('Mobile a11y — focus trap on LightboxModal', () => { test('Escape closes the lightbox and Tab cycles inside', async ({ page, guest, signIn }) => { const g = await guest('FocusTrap'); await seedUpload(g.jwt); await signIn(page, g); await page.goto('/feed'); const card = page.locator('article').filter({ hasText: g.displayName }).first(); const trigger = card.getByRole('button', { name: 'Bild vergrößern' }); await expect(trigger).toBeVisible({ timeout: 10_000 }); await trigger.click(); // Lightbox is role="dialog" aria-modal="true". const lightbox = page.locator('[role="dialog"][aria-modal="true"]').filter({ has: page.locator('img, video') }); await expect(lightbox).toBeVisible(); // After opening, focus should be inside the lightbox (trap autoFocus moves // focus to the first focusable). Verify by checking activeElement is // contained. await expect.poll(async () => { return await page.evaluate(() => { const dlg = document.querySelector('[role="dialog"][aria-modal="true"]'); return !!dlg && dlg.contains(document.activeElement); }); }, { timeout: 2_000 }).toBe(true); // Escape dismisses the lightbox. await page.keyboard.press('Escape'); await expect(lightbox).not.toBeVisible({ timeout: 2_000 }); }); });