Modern QA2026Focus Trap Testing — tiles
Log inJoin
35 / 74 · 10 Visual & Accessibility Testing · Keyboard Navigation Testing← prev⊞ allnext →☰ Read as one page

7.3Focus Trap Testing

Modal dialogs must trap focus -- Tab should cycle through elements inside the modal without escaping to the page behind it. This prevents users from accidentally interacting with content they cannot see.

test('focus trap works in modal dialogs', async ({ page }) => {
    await page.goto('/');

    // Open a modal
    await page.click('[data-testid="open-modal"]');
    await expect(page.locator('[role="dialog"]')).toBeVisible();

    // Collect all focusable elements in the modal
    const modalFocusable = await page.evaluate(() => {
        const modal = document.querySelector('[role="dialog"]');
        if (!modal) return [];
        const focusable = modal.querySelectorAll(
            'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        return Array.from(focusable).map(el => ({
            tag: el.tagName,
            text: el.textContent?.trim().substring(0, 20),
        }));
    });

    expect(modalFocusable.length).toBeGreaterThan(0);

    // Tab through all elements -- focus should cycle within the modal
    for (let i = 0; i < modalFocusable.length + 2; i++) {
        await page.keyboard.press('Tab');
        const isInModal = await page.evaluate(() => {
            const modal = document.querySelector('[role="dialog"]');
            return modal?.contains(document.activeElement);
        });
        expect(isInModal).toBe(true);
    }

    // Escape should close the modal
    await page.keyboard.press('Escape');
    await expect(page.locator('[role="dialog"]')).not.toBeVisible();

    // Focus should return to the trigger element
    const focusedAfterClose = await page.evaluate(() =>
        document.activeElement?.getAttribute('data-testid')
    );
    expect(focusedAfterClose).toBe('open-modal');
});