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');
});