import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { ModalShell } from '../ModalShell'
import { topmostDialog } from '../focusable'
afterEach(cleanup)
it('traps focus, closes on Escape, and restores focus to the launcher', () => {
const launcher = document.createElement('button')
document.body.appendChild(launcher)
launcher.focus()
const onClose = vi.fn()
const content = (open: boolean) =>
Test dialog
const { rerender } = render(content(true))
const dialog = screen.getByRole('dialog', { name: 'Test dialog' })
expect(dialog).toHaveFocus()
launcher.focus()
expect(dialog).toHaveFocus()
const [first, last] = screen.getAllByRole('button').filter(button => button !== launcher)
fireEvent.keyDown(document, { key: 'Tab', shiftKey: true })
expect(last).toHaveFocus()
last.focus()
fireEvent.keyDown(document, { key: 'Tab' })
expect(first).toHaveFocus()
fireEvent.keyDown(document, { key: 'Escape' })
expect(onClose).toHaveBeenCalledOnce()
rerender(content(false))
expect(launcher).toHaveFocus()
launcher.remove()
})
it('leaves Escape and Tab to a dialog opened above it', async () => {
const onClose = vi.fn()
render(Parent
)
const nested = document.createElement('div')
nested.setAttribute('role', 'dialog')
nested.setAttribute('aria-modal', 'true')
nested.style.position = 'fixed'
nested.style.zIndex = '1100'
const input = document.createElement('input')
nested.appendChild(input)
document.body.appendChild(nested)
const parent = document.querySelector('[aria-labelledby="dialog-title"]')!
await waitFor(() => {
expect(parent).toHaveAttribute('aria-modal', 'false')
expect(parent).toHaveAttribute('aria-hidden', 'true')
})
input.focus()
fireEvent.keyDown(document, { key: 'Escape' })
fireEvent.keyDown(document, { key: 'Tab' })
expect(onClose).not.toHaveBeenCalled()
expect(input).toHaveFocus()
input.blur()
fireEvent.keyDown(document, { key: 'Escape' })
expect(onClose).not.toHaveBeenCalled()
nested.remove()
await waitFor(() => {
expect(parent).toHaveAttribute('aria-modal', 'true')
expect(parent).not.toHaveAttribute('aria-hidden')
})
})
it('uses visual overlay order when a lower-layer dialog portals later', () => {
const account = document.createElement('div')
account.setAttribute('role', 'dialog')
account.style.zIndex = '1300'
const picker = document.createElement('div')
picker.setAttribute('role', 'dialog')
picker.style.zIndex = '1100'
document.body.append(account, picker)
expect(topmostDialog()).toBe(account)
account.remove()
picker.remove()
})