Files
Genarrative/src/components/image-editor/EditorAgentConversation/ToolCallView.test.tsx
T
k88936 386534a415
Project CI / Frontend tests (push) Failing after 22s
Project CI / Repository checks (push) Successful in 1m10s
Project CI / Backend tests (push) Successful in 3m40s
Project CI / Native shell tests (push) Successful in 10m56s
Feat/artagent 点击/右键功能 聚焦到画布上的素材 (#123)
- 为避免prop drilling把原有的刷新画布集中到一个useContext中,
   在此基础上实现画布聚焦, 并在画布agent中使用

![shotmd-1785482938-compressed.webp](/attachments/59d71873-79fc-40e8-a9dc-7830a5ad7f3c)

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/123
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-31 17:25:35 +08:00

266 lines
7.8 KiB
TypeScript

/* @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<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((nextResolve) => {
resolve = nextResolve;
});
return { promise, resolve };
}
function createToolCall(
overrides: Partial<EditorAgentToolCall> = {},
): 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 }) => (
<ImageCanvasActionsProvider
focusResource={focusResourceMock}
refreshCanvas={refreshCanvasMock}
>
{children}
</ImageCanvasActionsProvider>
),
});
}
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<ReturnType<typeof createCompletedJobResponse>>();
getExternalGenerationJobStatusMock.mockReturnValueOnce(
pendingResponse.promise,
);
const onJobCompleted = vi.fn();
const { rerender } = renderWithCanvasActions(
<ToolCallView
toolCall={createToolCall()}
onJobCompleted={onJobCompleted}
/>,
);
await waitFor(() => {
expect(getExternalGenerationJobStatusMock).toHaveBeenCalledWith(
'job-a',
);
});
rerender(
<ToolCallView
toolCall={createToolCall({ status, error })}
onJobCompleted={onJobCompleted}
/>,
);
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<ReturnType<typeof createCompletedJobResponse>>();
const jobBResponse =
createDeferred<ReturnType<typeof createCompletedJobResponse>>();
getExternalGenerationJobStatusMock.mockImplementation((jobId: string) =>
jobId === 'job-a' ? jobAResponse.promise : jobBResponse.promise,
);
const onJobCompleted = vi.fn();
const { rerender } = renderWithCanvasActions(
<ToolCallView
toolCall={createToolCall()}
onJobCompleted={onJobCompleted}
/>,
);
await waitFor(() => {
expect(getExternalGenerationJobStatusMock).toHaveBeenCalledWith('job-a');
});
rerender(
<ToolCallView
toolCall={createToolCall({ externalJobId: 'job-b' })}
onJobCompleted={onJobCompleted}
/>,
);
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(
<ToolCallView
toolCall={createToolCall({
status: 'completed',
externalJobId: null,
images: [
{
resourceId: ' resource-image ',
imageSrc: 'data:image/png;base64,aW1hZ2U=',
},
{
imageSrc: 'data:image/png;base64,bGVnYWN5',
},
],
videos: [
{
resourceId: 'resource-video',
videoSrc: 'data:video/mp4;base64,dmlkZW8=',
},
],
audios: [
{
resourceId: 'resource-audio',
audioSrc: 'data:audio/wav;base64,YXVkaW8=',
},
],
})}
onRightClickMenu={onRightClickMenu}
/>,
);
const imageCards = container.querySelectorAll('.grid.grid-cols-3 > 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,
}),
]);
});
});