Single-click original preview in the composer rail and chat history; remove control inside the thumbnail, revealed on hover/focus (always on touch); hidden-scrollbar rail overflow paged by edge arrows with wheel panning and end-reveal on add; image-intake rejections and prompt failures announce as a transient top-center toast instead of inline strips. The attachment atoms move to a new zero-cordis package @deepseek-ai/dsh-client-ui-attachment (rail, message gallery, lightbox), seeded as a platform module; the toast is a ui-primitives atom. Strings arrive as label props bridged from the conversation dictionary.
51 lines
2.0 KiB
TypeScript
51 lines
2.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { cleanup, fireEvent, render } from '@testing-library/react'
|
|
import { ImageLightbox } from '../src/ImageLightbox.tsx'
|
|
|
|
afterEach(cleanup)
|
|
|
|
const labels = { dialog: '原图预览', close: '关闭原图预览' }
|
|
|
|
describe('ImageLightbox', () => {
|
|
it('focuses its close control, closes by button and Escape, and restores focus', () => {
|
|
const opener = document.createElement('button')
|
|
document.body.appendChild(opener)
|
|
opener.focus()
|
|
const onClose = vi.fn()
|
|
const view = render(<ImageLightbox src="blob:original" alt="原图" labels={labels} onClose={onClose} />)
|
|
const close = view.getByRole('button', { name: '关闭原图预览' })
|
|
expect(document.activeElement).toBe(close)
|
|
fireEvent.keyDown(window, { key: 'a' })
|
|
expect(onClose).not.toHaveBeenCalled()
|
|
fireEvent.keyDown(window, { key: 'Escape' })
|
|
fireEvent.click(close)
|
|
expect(onClose).toHaveBeenCalledTimes(2)
|
|
view.unmount()
|
|
expect(document.activeElement).toBe(opener)
|
|
opener.remove()
|
|
})
|
|
|
|
it('tolerates a focus owner it cannot restore (no active element at mount)', () => {
|
|
// jsdom always reports body as the fallback active element; stub the
|
|
// element-less state a detached focus can leave.
|
|
Object.defineProperty(document, 'activeElement', { configurable: true, get: () => null })
|
|
try {
|
|
const view = render(<ImageLightbox src="blob:original" alt="原图" labels={labels} onClose={vi.fn()} />)
|
|
view.unmount()
|
|
} finally {
|
|
delete (document as { activeElement?: unknown }).activeElement
|
|
}
|
|
})
|
|
|
|
it('closes on a backdrop press but not on a press over the image', () => {
|
|
const onClose = vi.fn()
|
|
const view = render(<ImageLightbox src="blob:original" alt="原图" labels={labels} onClose={onClose} />)
|
|
fireEvent.mouseDown(view.getByRole('img'))
|
|
expect(onClose).not.toHaveBeenCalled()
|
|
fireEvent.mouseDown(view.getByRole('dialog', { name: '原图预览' }))
|
|
expect(onClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|