/** * E2E regression tests for PersonTypeahead dropdown visibility. * * These tests verify that the dropdown list is never clipped by a parent * container's stacking context — the root cause of issue #343. * * The tests run at both desktop (1280×720) and tablet (768×1024) viewports * as required by the acceptance criteria. */ import { test, expect, type Page } from '@playwright/test'; /** * Find a document edit URL to use as the test page. * Falls back to /documents/new if no existing document is found. */ async function getDocumentEditUrl(page: Page): Promise { await page.goto('/'); await page.waitForLoadState('networkidle'); const firstDocLink = page.locator('a[href^="/documents/"]').first(); const href = await firstDocLink.getAttribute('href').catch(() => null); if (href) { return `${href}/edit`; } return '/documents/new'; } /** Wait for the listbox to become visible after triggering a search. */ async function waitForListbox(page: Page): Promise { await page.waitForSelector('[role="listbox"]', { state: 'visible', timeout: 2000 }); } test.describe('PersonTypeahead — dropdown visibility (desktop)', () => { test.use({ viewport: { width: 1280, height: 720 } }); test('sender dropdown items are visible and not clipped in document edit', async ({ page }) => { const editUrl = await getDocumentEditUrl(page); await page.goto(editUrl); await page.waitForLoadState('networkidle'); // Find the sender typeahead input (the visible text input, not the hidden one) const senderInput = page.locator('#senderId-search'); await expect(senderInput).toBeVisible(); // Type to trigger the dropdown await senderInput.click(); await senderInput.fill('a'); // Wait for the dropdown to appear (handles debounce automatically) await waitForListbox(page); const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); const firstOption = dropdown.locator('[role="option"]').first(); await expect(firstOption).toBeVisible(); // Verify the bounding box is within the viewport (not clipped) const box = await firstOption.boundingBox(); expect(box).not.toBeNull(); expect(box!.y).toBeGreaterThan(0); expect(box!.y + box!.height).toBeLessThan(720); await page.screenshot({ path: 'test-results/e2e/person-typeahead-dropdown-desktop.png' }); }); test('dropdown is positioned below the input field (not hidden behind parent)', async ({ page }) => { const editUrl = await getDocumentEditUrl(page); await page.goto(editUrl); await page.waitForLoadState('networkidle'); const senderInput = page.locator('#senderId-search'); await expect(senderInput).toBeVisible(); const inputBox = await senderInput.boundingBox(); expect(inputBox).not.toBeNull(); await senderInput.click(); await senderInput.fill('a'); await waitForListbox(page); const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); const dropdownBox = await dropdown.boundingBox(); expect(dropdownBox).not.toBeNull(); // Dropdown must appear below the input, not on top or clipped behind it expect(dropdownBox!.y).toBeGreaterThanOrEqual(inputBox!.y + inputBox!.height - 5); await page.screenshot({ path: 'test-results/e2e/person-typeahead-dropdown-position.png' }); }); }); test.describe('PersonTypeahead — dropdown visibility (tablet)', () => { test.use({ viewport: { width: 768, height: 1024 } }); test('sender dropdown items are visible and not clipped on tablet viewport', async ({ page }) => { const editUrl = await getDocumentEditUrl(page); await page.goto(editUrl); await page.waitForLoadState('networkidle'); const senderInput = page.locator('#senderId-search'); await expect(senderInput).toBeVisible(); await senderInput.click(); await senderInput.fill('a'); await waitForListbox(page); const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); const firstOption = dropdown.locator('[role="option"]').first(); await expect(firstOption).toBeVisible(); const box = await firstOption.boundingBox(); expect(box).not.toBeNull(); expect(box!.y).toBeGreaterThan(0); expect(box!.y + box!.height).toBeLessThan(1024); await page.screenshot({ path: 'test-results/e2e/person-typeahead-dropdown-tablet.png' }); }); }); test.describe('PersonTypeahead — keyboard navigation', () => { test.use({ viewport: { width: 1280, height: 720 } }); test('ArrowDown moves focus to the first option', async ({ page }) => { const editUrl = await getDocumentEditUrl(page); await page.goto(editUrl); await page.waitForLoadState('networkidle'); const senderInput = page.locator('#senderId-search'); await senderInput.click(); await senderInput.fill('a'); await waitForListbox(page); await senderInput.press('ArrowDown'); // First option should now be the active descendant const activeDescendant = await senderInput.getAttribute('aria-activedescendant'); expect(activeDescendant).toBeTruthy(); await page.screenshot({ path: 'test-results/e2e/person-typeahead-keyboard-nav.png' }); }); test('Escape key closes the dropdown', async ({ page }) => { const editUrl = await getDocumentEditUrl(page); await page.goto(editUrl); await page.waitForLoadState('networkidle'); const senderInput = page.locator('#senderId-search'); await senderInput.click(); await senderInput.fill('a'); await waitForListbox(page); const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); await senderInput.press('Escape'); await expect(dropdown).not.toBeVisible(); }); test('aria-expanded is true when dropdown is open', async ({ page }) => { const editUrl = await getDocumentEditUrl(page); await page.goto(editUrl); await page.waitForLoadState('networkidle'); const senderInput = page.locator('#senderId-search'); // Initially closed const initialExpanded = await senderInput.getAttribute('aria-expanded'); expect(initialExpanded).toBe('false'); await senderInput.click(); await senderInput.fill('a'); await waitForListbox(page); const expanded = await senderInput.getAttribute('aria-expanded'); expect(expanded).toBe('true'); }); }); test.describe('PersonTypeahead — click-outside dismiss (fixed position)', () => { test.use({ viewport: { width: 1280, height: 720 } }); test('clicking outside a fixed-position dropdown closes it', async ({ page }) => { const editUrl = await getDocumentEditUrl(page); await page.goto(editUrl); await page.waitForLoadState('networkidle'); const senderInput = page.locator('#senderId-search'); await senderInput.click(); await senderInput.fill('a'); await waitForListbox(page); const dropdown = page.locator('[role="listbox"]').first(); await expect(dropdown).toBeVisible(); // Click somewhere else on the page await page.click('body', { position: { x: 10, y: 10 } }); await expect(dropdown).not.toBeVisible(); }); });