|
| 1 | +// @vitest-environment jsdom |
| 2 | +import React, { act } from 'react'; |
| 3 | +import { createRoot, type Root } from 'react-dom/client'; |
| 4 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
| 5 | +import { UserMessageImage } from './UserMessageImage'; |
| 6 | + |
| 7 | +const { readFileContent } = vi.hoisted(() => ({ readFileContent: vi.fn() })); |
| 8 | +vi.mock('@/infrastructure/api/service-api/WorkspaceAPI', () => ({ workspaceAPI: { readFileContent } })); |
| 9 | +vi.mock('@/infrastructure/i18n', () => ({ useI18n: () => ({ t: (_key: string, values: { message: string }) => `Load failed: ${values.message}` }) })); |
| 10 | +vi.mock('@/shared/utils/logger', () => ({ createLogger: () => ({ warn: vi.fn(), debug: vi.fn(), info: vi.fn(), error: vi.fn() }) })); |
| 11 | + |
| 12 | +const image = { id: 'image', name: 'Photo.png', imagePath: '/Lark images/Photo.png', mimeType: 'image/png' }; |
| 13 | + |
| 14 | +describe('UserMessageImage', () => { |
| 15 | + let container: HTMLDivElement; |
| 16 | + let root: Root; |
| 17 | + beforeEach(() => { |
| 18 | + globalThis.IS_REACT_ACT_ENVIRONMENT = true; |
| 19 | + readFileContent.mockReset(); |
| 20 | + container = document.createElement('div'); |
| 21 | + document.body.appendChild(container); |
| 22 | + root = createRoot(container); |
| 23 | + }); |
| 24 | + afterEach(() => { act(() => root.unmount()); container.remove(); }); |
| 25 | + const render = async (element: React.ReactNode) => { await act(async () => { root.render(element); }); }; |
| 26 | + |
| 27 | + |
| 28 | + it('reads host image bytes through the transport and previews the same data URL', async () => { |
| 29 | + readFileContent.mockResolvedValue('aW1hZ2U='); |
| 30 | + const onPreview = vi.fn(); |
| 31 | + await render(<UserMessageImage image={image} onPreview={onPreview} />); |
| 32 | + const thumbnail = container.querySelector('img')!; |
| 33 | + expect(readFileContent).toHaveBeenCalledWith(image.imagePath, 'base64'); |
| 34 | + expect(thumbnail.getAttribute('src')).toBe('data:image/png;base64,aW1hZ2U='); |
| 35 | + act(() => thumbnail.click()); |
| 36 | + expect(onPreview).toHaveBeenCalledWith(thumbnail.getAttribute('src')); |
| 37 | + }); |
| 38 | + |
| 39 | + it('uses embedded clipboard data without reading a host file', async () => { |
| 40 | + await render(<UserMessageImage image={{ ...image, dataUrl: 'data:image/png;base64,AA==' }} onPreview={vi.fn()} />); |
| 41 | + expect(container.querySelector('img')!.getAttribute('src')).toBe('data:image/png;base64,AA=='); |
| 42 | + expect(readFileContent).not.toHaveBeenCalled(); |
| 43 | + }); |
| 44 | + |
| 45 | + it('displays read failures without falling back to controller-local URLs', async () => { |
| 46 | + readFileContent.mockRejectedValue(new Error('Host offline')); |
| 47 | + await render(<UserMessageImage image={image} onPreview={vi.fn()} />); |
| 48 | + expect(container.querySelector('[role=alert]')!.textContent).toContain('Host offline'); |
| 49 | + expect(container.querySelector('img')).toBeNull(); |
| 50 | + expect(readFileContent).toHaveBeenCalledTimes(1); |
| 51 | + }); |
| 52 | + |
| 53 | + it('ignores a stale image read after the attachment changes', async () => { |
| 54 | + let finishOld!: (content: string) => void; |
| 55 | + readFileContent.mockImplementationOnce(() => new Promise<string>(resolve => { finishOld = resolve; })); |
| 56 | + readFileContent.mockResolvedValueOnce('NEW'); |
| 57 | + await render(<UserMessageImage image={image} onPreview={vi.fn()} />); |
| 58 | + await render(<UserMessageImage image={{ ...image, imagePath: '/new.png' }} onPreview={vi.fn()} />); |
| 59 | + expect(container.querySelector('img')!.getAttribute('src')).toBe('data:image/png;base64,NEW'); |
| 60 | + await act(async () => { finishOld('OLD'); }); |
| 61 | + expect(container.querySelector('img')!.getAttribute('src')).toBe('data:image/png;base64,NEW'); |
| 62 | + }); |
| 63 | +}); |
0 commit comments