需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
220 lines
7.9 KiB
TypeScript
220 lines
7.9 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
|
import { ImageCanvas } from '@/pages/ImageCanvas';
|
|
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
|
import type {
|
|
DesignGenerationTask,
|
|
DesignWorkspace,
|
|
DesignWorkspaceBootstrap,
|
|
} from '../../shared/image-workspace';
|
|
|
|
const fetchImageWorkspaceMock = vi.hoisted(() => vi.fn());
|
|
const fetchImageWorkspaceProjectMock = vi.hoisted(() => vi.fn());
|
|
const fetchImageWorkspaceTasksMock = vi.hoisted(() => vi.fn());
|
|
const sendImageWorkspaceMessageMock = vi.hoisted(() => vi.fn());
|
|
const confirmImageWorkspaceGenerationMock = vi.hoisted(() => vi.fn());
|
|
const resolveImageWorkspaceAssetUrlMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('@/lib/image-workspace', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/lib/image-workspace')>();
|
|
return {
|
|
...actual,
|
|
fetchImageWorkspace: (...args: unknown[]) => fetchImageWorkspaceMock(...args),
|
|
fetchImageWorkspaceProject: (...args: unknown[]) => fetchImageWorkspaceProjectMock(...args),
|
|
fetchImageWorkspaceTasks: (...args: unknown[]) => fetchImageWorkspaceTasksMock(...args),
|
|
sendImageWorkspaceMessage: (...args: unknown[]) => sendImageWorkspaceMessageMock(...args),
|
|
confirmImageWorkspaceGeneration: (...args: unknown[]) => (
|
|
confirmImageWorkspaceGenerationMock(...args)
|
|
),
|
|
resolveImageWorkspaceAssetUrl: (...args: unknown[]) => (
|
|
resolveImageWorkspaceAssetUrlMock(...args)
|
|
),
|
|
};
|
|
});
|
|
|
|
const bootstrapFixture: DesignWorkspaceBootstrap = {
|
|
capabilities: {
|
|
conversation: true,
|
|
generation: true,
|
|
image: true,
|
|
video: true,
|
|
},
|
|
workspaces: [{
|
|
workspaceId: 'workspace-cloud',
|
|
title: '云端角色设定',
|
|
turnRevision: 1,
|
|
viewRevision: 1,
|
|
phase: 'awaiting_confirmation',
|
|
brief: {
|
|
version: 1,
|
|
status: 'ready',
|
|
medium: 'image',
|
|
summary: '夜色中的机械城堡角色海报',
|
|
ready: true,
|
|
missingDecision: null,
|
|
},
|
|
updatedAt: '2026-07-31T10:00:00Z',
|
|
}],
|
|
};
|
|
|
|
function workspaceFixture(): DesignWorkspace {
|
|
return {
|
|
...bootstrapFixture.workspaces[0],
|
|
messages: [
|
|
{
|
|
id: 'message-user',
|
|
role: 'user',
|
|
kind: 'user',
|
|
text: '做一张夜色中的机械城堡角色海报',
|
|
quickReplies: [],
|
|
generationQuote: null,
|
|
turnRevision: 1,
|
|
createdAt: '2026-07-31T10:00:00Z',
|
|
},
|
|
{
|
|
id: 'message-assistant',
|
|
role: 'assistant',
|
|
kind: 'confirmation',
|
|
text: '方向已经明确,确认后开始生成。',
|
|
quickReplies: ['确认生成', '继续调整'],
|
|
generationQuote: {
|
|
quoteId: 'quote-one',
|
|
status: 'active',
|
|
medium: 'image',
|
|
briefVersion: 1,
|
|
briefSummary: '夜色中的机械城堡角色海报',
|
|
quotedDesignPoints: 1,
|
|
expiresAt: '2026-07-31T11:00:00Z',
|
|
},
|
|
turnRevision: 1,
|
|
createdAt: '2026-07-31T10:00:01Z',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const taskFixture: DesignGenerationTask = {
|
|
taskId: 'task-one',
|
|
workspaceId: 'workspace-cloud',
|
|
medium: 'image',
|
|
status: 'succeeded',
|
|
briefVersion: 1,
|
|
briefSummary: '夜色中的机械城堡角色海报',
|
|
quoteId: 'quote-old',
|
|
quotedDesignPoints: 1,
|
|
failureCode: null,
|
|
resultAssets: [{
|
|
assetId: 'asset-one',
|
|
workspaceId: 'workspace-cloud',
|
|
mediaType: 'image',
|
|
mimeType: 'image/png',
|
|
width: 1024,
|
|
height: 1024,
|
|
durationMilliseconds: null,
|
|
createdAt: '2026-07-31T09:00:00Z',
|
|
contentPath: '/api/works/image-workspace/workspaces/workspace-cloud/assets/asset-one/content',
|
|
}],
|
|
createdAt: '2026-07-31T09:00:00Z',
|
|
updatedAt: '2026-07-31T09:01:00Z',
|
|
};
|
|
|
|
describe('ImageCanvas Workspace-first design experience', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
useImageWorkspaceStore.getState().reset();
|
|
fetchImageWorkspaceMock.mockResolvedValue(bootstrapFixture);
|
|
fetchImageWorkspaceProjectMock.mockResolvedValue(workspaceFixture());
|
|
fetchImageWorkspaceTasksMock.mockResolvedValue([taskFixture]);
|
|
sendImageWorkspaceMessageMock.mockResolvedValue(workspaceFixture());
|
|
confirmImageWorkspaceGenerationMock.mockResolvedValue({
|
|
...workspaceFixture(),
|
|
turnRevision: 2,
|
|
phase: 'shaping',
|
|
});
|
|
resolveImageWorkspaceAssetUrlMock.mockResolvedValue(
|
|
'http://127.0.0.1:13210/content?token=host',
|
|
);
|
|
});
|
|
|
|
it('shows an honest unavailable state without fabricating projects', async () => {
|
|
fetchImageWorkspaceMock.mockRejectedValueOnce(new ImageWorkspaceApiError(
|
|
501,
|
|
'IMAGE_WORKSPACE_UNAVAILABLE',
|
|
'AI 设计暂不可用',
|
|
));
|
|
|
|
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
|
|
|
expect(await screen.findByTestId('image-workspace-unavailable'))
|
|
.toHaveTextContent('AI 设计暂不可用');
|
|
expect(screen.getByText(/暂时无法连接设计服务/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders one fixed design Agent, a Quote, and the unified image/video task list', async () => {
|
|
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
|
|
|
expect(await screen.findByText('云端角色设定')).toBeInTheDocument();
|
|
expect(screen.getByTestId('image-workspace-conversation'))
|
|
.toHaveTextContent('方向已经明确');
|
|
expect(screen.getByText('设计 Agent')).toBeInTheDocument();
|
|
expect(screen.getByTestId('design-quote-quote-one')).toHaveTextContent('1 设计点');
|
|
expect(screen.getByTestId('design-task-list')).toHaveTextContent('图片与视频任务统一展示');
|
|
expect(screen.getByTestId('design-task-task-one')).toHaveTextContent('已完成');
|
|
await waitFor(() => expect(screen.getByRole('img', { name: 'AI 设计生成结果' }))
|
|
.toHaveAttribute('src', 'http://127.0.0.1:13210/content?token=host'));
|
|
expect(screen.queryByText('生成设置')).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/当前 Agent/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('explains the project model before the first Workspace is created', async () => {
|
|
fetchImageWorkspaceMock.mockResolvedValueOnce({
|
|
...bootstrapFixture,
|
|
workspaces: [],
|
|
});
|
|
|
|
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
|
|
|
expect(await screen.findByText('创建第一个设计项目')).toBeInTheDocument();
|
|
expect(screen.getByText(/持续保留与设计 Agent 的对话/)).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: '新建设计项目' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('sends text to the Agent with the current Turn revision instead of generating directly', async () => {
|
|
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
|
await screen.findByText('云端角色设定');
|
|
|
|
fireEvent.change(screen.getByLabelText('设计需求'), {
|
|
target: { value: '把主角换成暖色轮廓光' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
|
|
|
|
await waitFor(() => expect(sendImageWorkspaceMessageMock).toHaveBeenCalledWith(
|
|
'workspace-cloud',
|
|
1,
|
|
'把主角换成暖色轮廓光',
|
|
));
|
|
expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('creates a generation task only through explicit Quote confirmation', async () => {
|
|
fetchImageWorkspaceTasksMock
|
|
.mockResolvedValueOnce([taskFixture])
|
|
.mockResolvedValueOnce([{
|
|
...taskFixture,
|
|
taskId: 'task-two',
|
|
status: 'queued',
|
|
resultAssets: [],
|
|
}]);
|
|
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
|
await screen.findByTestId('design-quote-quote-one');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '确认并开始生成' }));
|
|
|
|
await waitFor(() => expect(confirmImageWorkspaceGenerationMock)
|
|
.toHaveBeenCalledWith('workspace-cloud', 1, 'quote-one'));
|
|
await waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2));
|
|
});
|
|
});
|