|
| 1 | +/** |
| 2 | + * @vitest-environment jsdom |
| 3 | + */ |
| 4 | +import { act } from 'react' |
| 5 | +import { createRoot } from 'react-dom/client' |
| 6 | +import { afterEach, describe, expect, it, vi } from 'vitest' |
| 7 | +import { ChatFileDownload } from '@/app/(interfaces)/chat/components/message/components/file-download' |
| 8 | +import type { ChatFile } from '@/app/(interfaces)/chat/components/message/message' |
| 9 | + |
| 10 | +const imageFile: ChatFile = { |
| 11 | + id: 'file-image', |
| 12 | + name: 'generated.png', |
| 13 | + key: 'execution/generated.png', |
| 14 | + url: 'https://files.example.com/generated.png', |
| 15 | + size: 3, |
| 16 | + type: 'image/png', |
| 17 | + base64: 'YWJj', |
| 18 | +} |
| 19 | + |
| 20 | +const mounts: Array<() => void> = [] |
| 21 | + |
| 22 | +function renderFile(file: ChatFile): HTMLDivElement { |
| 23 | + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true |
| 24 | + const container = document.createElement('div') |
| 25 | + const root = createRoot(container) |
| 26 | + act(() => root.render(<ChatFileDownload file={file} />)) |
| 27 | + mounts.push(() => act(() => root.unmount())) |
| 28 | + return container |
| 29 | +} |
| 30 | + |
| 31 | +afterEach(() => { |
| 32 | + while (mounts.length) mounts.pop()?.() |
| 33 | + vi.restoreAllMocks() |
| 34 | +}) |
| 35 | + |
| 36 | +describe('ChatFileDownload', () => { |
| 37 | + it('previews returned image bytes inline without requiring a workspace session', () => { |
| 38 | + const container = renderFile(imageFile) |
| 39 | + const image = container.querySelector('img') |
| 40 | + expect(image?.getAttribute('src')).toBe('data:image/png;base64,YWJj') |
| 41 | + expect(image?.alt).toBe('generated.png') |
| 42 | + expect(container.querySelector('button')?.textContent).toContain('generated.png') |
| 43 | + }) |
| 44 | + |
| 45 | + it('uses the file URL when inline bytes are unavailable', () => { |
| 46 | + const container = renderFile({ ...imageFile, base64: undefined }) |
| 47 | + expect(container.querySelector('img')?.getAttribute('src')).toBe(imageFile.url) |
| 48 | + }) |
| 49 | + |
| 50 | + it('uses the canonical serve route for unsafe file URLs', () => { |
| 51 | + const container = renderFile({ ...imageFile, base64: undefined, url: 'javascript:alert(1)' }) |
| 52 | + expect(container.querySelector('img')?.getAttribute('src')).toBe( |
| 53 | + '/api/files/serve/execution%2Fgenerated.png?context=execution' |
| 54 | + ) |
| 55 | + }) |
| 56 | + |
| 57 | + it('keeps a download available when an image preview fails', () => { |
| 58 | + const container = renderFile(imageFile) |
| 59 | + act(() => container.querySelector('img')!.dispatchEvent(new Event('error'))) |
| 60 | + expect(container.querySelector('img')).toBeNull() |
| 61 | + expect(container.querySelector('button')?.textContent).toContain('generated.png') |
| 62 | + }) |
| 63 | + |
| 64 | + it('renders documents as downloads without an image preview', () => { |
| 65 | + const container = renderFile({ ...imageFile, name: 'report.pdf', type: 'application/pdf' }) |
| 66 | + expect(container.querySelector('img')).toBeNull() |
| 67 | + expect(container.querySelector('button')?.textContent).toContain('report.pdf') |
| 68 | + }) |
| 69 | +}) |
0 commit comments