import { useLayoutEffect } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
import { VIEWER_ACTION_CLASS } from './viewerChromeClasses'
import ViewerShell from './ViewerShell'
const dialogPrototype = HTMLDialogElement.prototype
const originalDialogDescriptors = {
close: Object.getOwnPropertyDescriptor(dialogPrototype, 'close'),
showModal: Object.getOwnPropertyDescriptor(dialogPrototype, 'showModal'),
}
const defineDialogMethod = (
name: 'close' | 'showModal',
implementation: () => void
) => {
Object.defineProperty(dialogPrototype, name, {
configurable: true,
value: implementation,
writable: true,
})
}
const restoreDialogMethod = (
name: 'close' | 'showModal',
descriptor: PropertyDescriptor | undefined
) => {
if (descriptor) Object.defineProperty(dialogPrototype, name, descriptor)
else Reflect.deleteProperty(dialogPrototype, name)
}
const PreOpenDialog = () => {
useLayoutEffect(() => {
document.querySelector('[data-testid="viewer"]')?.setAttribute('open', '')
}, [])
return null
}
afterEach(() => {
restoreDialogMethod('close', originalDialogDescriptors.close)
restoreDialogMethod('showModal', originalDialogDescriptors.showModal)
vi.restoreAllMocks()
})
describe('ViewerShell', () => {
it('drives open state, chrome content, and forwarded attributes', () => {
const { rerender } = renderWithProviders(
Action}
ariaLabel="report.pdf"
data-testid="viewer"
>
content
)
const dialog = screen.getByTestId('viewer')
expect(dialog).toHaveAttribute('open')
expect(dialog).toHaveAttribute('aria-label', 'report.pdf')
expect(dialog).toHaveTextContent('2 / 3')
expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Close viewer' })).toHaveClass(
VIEWER_ACTION_CLASS
)
rerender(
content
)
expect(dialog).not.toHaveAttribute('open')
})
it('focuses close on open and restores the previous focus on close', () => {
const onClose = vi.fn()
const previouslyFocused = document.createElement('button')
document.body.appendChild(previouslyFocused)
previouslyFocused.focus()
const { rerender } = renderWithProviders(
content
)
rerender(
content
)
expect(screen.getByRole('button', { name: 'Close viewer' })).toHaveFocus()
rerender(
content
)
expect(previouslyFocused).toHaveFocus()
previouslyFocused.remove()
})
it('falls back to the open attribute when dialog methods are absent', () => {
Reflect.deleteProperty(dialogPrototype, 'showModal')
Reflect.deleteProperty(dialogPrototype, 'close')
const { rerender } = renderWithProviders(
content
)
const dialog = screen.getByTestId('viewer')
expect(dialog).toHaveAttribute('open')
rerender(
content
)
expect(dialog).not.toHaveAttribute('open')
})
it('falls back when showModal throws', () => {
const showModal = vi.fn(() => {
throw new Error('not supported')
})
defineDialogMethod('showModal', showModal)
const { container } = renderWithProviders(
content
)
expect(container.querySelector('dialog')).toHaveAttribute('open')
expect(showModal).toHaveBeenCalledOnce()
})
it('falls back when close throws during cleanup', () => {
const close = vi.fn(() => {
throw new Error('not supported')
})
defineDialogMethod('close', close)
Reflect.deleteProperty(dialogPrototype, 'showModal')
const { rerender } = renderWithProviders(
content
)
const dialog = screen.getByTestId('viewer')
rerender(
content
)
expect(close).toHaveBeenCalledOnce()
expect(dialog).not.toHaveAttribute('open')
})
it('falls back when close throws in the closed branch', () => {
const close = vi.fn(() => {
throw new Error('not supported')
})
defineDialogMethod('close', close)
renderWithProviders(
)
expect(close).toHaveBeenCalledOnce()
expect(screen.getByTestId('viewer')).not.toHaveAttribute('open')
})
it('does not call showModal when the dialog is already open', () => {
const showModal = vi.fn()
defineDialogMethod('showModal', showModal)
renderWithProviders(
)
expect(screen.getByTestId('viewer')).toHaveAttribute('open')
expect(showModal).not.toHaveBeenCalled()
})
it('does not restore focus to a detached element', () => {
const previouslyFocused = document.createElement('button')
document.body.appendChild(previouslyFocused)
previouslyFocused.focus()
const { rerender } = renderWithProviders(
content
)
rerender(
content
)
previouslyFocused.remove()
expect(() =>
rerender(
content
)
).not.toThrow()
expect(document.activeElement).not.toBe(previouslyFocused)
})
it('uses the default accessible label', () => {
renderWithProviders(
content
)
expect(screen.getByTestId('viewer')).toHaveAttribute(
'aria-label',
'Attachment viewer'
)
})
it('does not close a dialog that was never opened', () => {
const close = vi.fn()
defineDialogMethod('close', close)
renderWithProviders(
content
)
expect(close).not.toHaveBeenCalled()
})
it('closes from the close, native close, and backdrop events', () => {
const onClose = vi.fn()
renderWithProviders(
content
)
const dialog = screen.getByTestId('viewer')
fireEvent.click(screen.getByRole('button', { name: 'Close viewer' }))
fireEvent(dialog, new Event('close'))
fireEvent.click(dialog)
expect(onClose).toHaveBeenCalledTimes(3)
onClose.mockClear()
fireEvent.click(screen.getByText('content'))
expect(onClose).not.toHaveBeenCalled()
})
})