/* @vitest-environment jsdom */ import { act, fireEvent, render, screen, waitFor, } from '@testing-library/react'; import type { ReactElement, ReactNode } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { EditorAgentToolCall } from '@/packages/shared/src/contracts'; import { ImageCanvasActionsProvider } from '@/src/components/image-editor/ImageCanvasActionsProvider.tsx'; import ToolCallView from './ToolCallView.tsx'; const getExternalGenerationJobStatusMock = vi.hoisted(() => vi.fn()); const focusResourceMock = vi.fn(); const refreshCanvasMock = vi.fn(); vi.mock('@/src/services/external-generation', () => ({ getExternalGenerationJobStatus: getExternalGenerationJobStatusMock, })); function createDeferred() { let resolve!: (value: T) => void; const promise = new Promise((nextResolve) => { resolve = nextResolve; }); return { promise, resolve }; } function createToolCall( overrides: Partial = {}, ): EditorAgentToolCall { return { toolName: 'generate-image', status: 'not_completed', args: {}, displayArgs: { stringArgs: [], imageArgs: [], extras: { priceMudPoints: 1 }, }, externalJobId: 'job-a', images: [], error: null, ...overrides, }; } function createCompletedJobResponse(jobId: string) { return { job: { operationId: jobId, status: 'completed' as const, phaseLabel: '已完成', phaseDetail: '生成完成', progress: 100, error: null, updatedAtMicros: 1, }, }; } function renderWithCanvasActions(ui: ReactElement) { return render(ui, { wrapper: ({ children }: { children: ReactNode }) => ( {children} ), }); } describe('ToolCallView', () => { beforeEach(() => { getExternalGenerationJobStatusMock.mockReset(); focusResourceMock.mockReset(); refreshCanvasMock.mockReset(); }); it.each([ ['failed', '失败', '生成失败'], ['cancelled', '已取消', null], ] as const)( 'keeps the server %s state when the pending response arrives late', async (status, statusLabel, error) => { const pendingResponse = createDeferred>(); getExternalGenerationJobStatusMock.mockReturnValueOnce( pendingResponse.promise, ); const onJobCompleted = vi.fn(); const { rerender } = renderWithCanvasActions( , ); await waitFor(() => { expect(getExternalGenerationJobStatusMock).toHaveBeenCalledWith( 'job-a', ); }); rerender( , ); expect(await screen.findByText(statusLabel)).toBeTruthy(); await act(async () => { pendingResponse.resolve(createCompletedJobResponse('job-a')); await pendingResponse.promise; }); expect(screen.getByText(statusLabel)).toBeTruthy(); expect(screen.queryByText('已完成')).toBeNull(); expect(onJobCompleted).not.toHaveBeenCalled(); expect(refreshCanvasMock).not.toHaveBeenCalled(); }, ); it('ignores job A after switching to job B and completes job B once', async () => { const jobAResponse = createDeferred>(); const jobBResponse = createDeferred>(); getExternalGenerationJobStatusMock.mockImplementation((jobId: string) => jobId === 'job-a' ? jobAResponse.promise : jobBResponse.promise, ); const onJobCompleted = vi.fn(); const { rerender } = renderWithCanvasActions( , ); await waitFor(() => { expect(getExternalGenerationJobStatusMock).toHaveBeenCalledWith('job-a'); }); rerender( , ); await waitFor(() => { expect(getExternalGenerationJobStatusMock).toHaveBeenCalledWith('job-b'); }); await act(async () => { jobAResponse.resolve(createCompletedJobResponse('job-a')); await jobAResponse.promise; }); expect(screen.getByText('执行中')).toBeTruthy(); expect(onJobCompleted).not.toHaveBeenCalled(); await act(async () => { jobBResponse.resolve(createCompletedJobResponse('job-b')); await jobBResponse.promise; }); expect(await screen.findByText('已完成')).toBeTruthy(); expect(onJobCompleted).toHaveBeenCalledTimes(1); expect(refreshCanvasMock).toHaveBeenCalledTimes(1); }); it('focuses image clicks only and exposes canvas resources to the right-click menu', () => { const onRightClickMenu = vi.fn(); const { container } = renderWithCanvasActions( div'); const video = container.querySelector('video'); const audio = container.querySelector('audio'); expect(imageCards).toHaveLength(2); expect(video).toBeTruthy(); expect(audio).toBeTruthy(); expect(imageCards[0]!.getAttribute('role')).toBeNull(); expect(imageCards[0]!.getAttribute('tabindex')).toBeNull(); fireEvent.click(imageCards[0]!); fireEvent.keyDown(imageCards[0]!, { key: 'Enter' }); fireEvent.keyDown(imageCards[0]!, { key: ' ', code: 'Space' }); fireEvent.click(imageCards[1]!); fireEvent.click(video!); fireEvent.click(audio!); expect(focusResourceMock).toHaveBeenCalledTimes(1); expect(focusResourceMock).toHaveBeenCalledWith('resource-image'); expect( screen.queryByRole('button', { name: '在画布中定位Agent生成图片-1', }), ).toBeNull(); expect(video!.getAttribute('aria-label')).toBe('Agent生成视频-1'); expect(audio!.getAttribute('aria-label')).toBe('Agent生成音频-1'); fireEvent.contextMenu(imageCards[0]!); fireEvent.contextMenu(video!.parentElement!); fireEvent.contextMenu(audio!.parentElement!); fireEvent.contextMenu(imageCards[1]!); expect(onRightClickMenu.mock.calls.map((call) => call[1])).toEqual([ expect.objectContaining({ kind: 'generated_media', mediaType: 'image', resourceId: 'resource-image', }), expect.objectContaining({ kind: 'generated_media', mediaType: 'video', resourceId: 'resource-video', }), expect.objectContaining({ kind: 'generated_media', mediaType: 'audio', resourceId: 'resource-audio', }), expect.objectContaining({ kind: 'generated_media', mediaType: 'image', resourceId: null, }), ]); }); });