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(); 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(); 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(); 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(); 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(); 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(); 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)); }); });