// @vitest-environment jsdom import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render, waitFor } from '@testing-library/react' import { AttachmentId } from '@deepseek-ai/dsh-attachment' import { ImageGallery, MessageImage } from '../src/MessageImage.tsx' import type { MessageImageLabels } from '../src/MessageImage.tsx' afterEach(cleanup) const labels: MessageImageLabels = { image: '图片', open: '查看原图', openNamed: label => `${label},点击查看原图`, loading: '图片加载中…', loadFailed: '图片加载失败,点击重试', lightbox: { dialog: '原图预览', close: '关闭原图预览' }, } const attachment = { attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`), mediaType: 'image/png' as const, bytes: 68, width: 640, height: 320, name: 'history.png', } describe('MessageImage', () => { it('loads a session-authorized URL, bounds the thumbnail, and clicks into the original', async () => { const load = vi.fn().mockResolvedValue('blob:history') const view = render() const frame = view.getByRole('button', { name: 'history.png,点击查看原图' }) expect(frame.getAttribute('style')).toContain('width: 240px') expect(frame.getAttribute('style')).toContain('height: 120px') expect(frame.getAttribute('title')).toBe('查看原图') await waitFor(() => { expect(view.getByAltText('history.png')).toBeTruthy() }) expect(load).toHaveBeenCalledWith(attachment) fireEvent.click(frame) expect(view.getByRole('dialog', { name: '原图预览' })).toBeTruthy() fireEvent.click(view.getByRole('button', { name: '关闭原图预览' })) expect(view.queryByRole('dialog', { name: '原图预览' })).toBeNull() }) it('ignores a click while the thumbnail is still loading', () => { const load = vi.fn(() => new Promise(() => {})) const view = render() const frame = view.getByRole('button', { name: 'history.png,点击查看原图' }) expect(view.getByText('图片加载中…')).toBeTruthy() fireEvent.click(frame) expect(view.queryByRole('dialog')).toBeNull() }) it('falls back to the image label for an unnamed attachment', async () => { const { name: _named, ...unnamed } = attachment const load = vi.fn().mockResolvedValue('blob:unnamed') const view = render() await waitFor(() => { expect(view.getByAltText('图片')).toBeTruthy() }) expect(view.getByRole('button', { name: '图片,点击查看原图' })).toBeTruthy() }) it('surfaces a retry control when durable bytes cannot be read, including a failed retry', async () => { const load = vi.fn() .mockRejectedValueOnce(new Error('offline')) .mockRejectedValueOnce(new Error('still offline')) .mockResolvedValueOnce('blob:retry') const view = render() const retry = await view.findByRole('button', { name: '图片加载失败,点击重试' }) fireEvent.click(retry) const retryAgain = await view.findByRole('button', { name: '图片加载失败,点击重试' }) fireEvent.click(retryAgain) await waitFor(() => { expect(view.getByAltText('history.png')).toBeTruthy() }) expect(load).toHaveBeenCalledTimes(3) }) it('ignores a load settling after unmount', async () => { let resolve: ((url: string) => void) | undefined const load = vi.fn(() => new Promise((r) => { resolve = r })) const view = render() view.unmount() resolve?.('blob:late') await Promise.resolve() let reject: ((error: Error) => void) | undefined const failing = vi.fn(() => new Promise((_r, rej) => { reject = rej })) const second = render() second.unmount() reject?.(new Error('late failure')) await Promise.resolve() }) }) describe('ImageGallery', () => { it('renders nothing without images and an aligned wrapping group with them', async () => { const load = vi.fn().mockResolvedValue('blob:gallery') const empty = render() expect(empty.container.firstChild).toBeNull() const view = render( , ) expect(view.container.querySelector('[data-align="end"]')).not.toBeNull() await waitFor(() => { expect(view.getAllByAltText('history.png')).toHaveLength(2) }) }) })