import { fireEvent, render, screen, within } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ImageCanvas } from '@/pages/ImageCanvas'; import { useAuthStore } from '@/stores/auth'; import { useImagePromptMuseumStore } from '@/stores/image-prompt-museum'; import { useImageWorkspaceStore } from '@/stores/image-workspace'; import { designBootstrapFixture, designFormFixture, designQuoteFixture, designWorkspaceFixture, } from '../fixtures/design-workspace-v2'; vi.mock('@/lib/image-workspace', async (importOriginal) => { const original = await importOriginal(); return { ...original, resolveImageWorkspaceAssetUrl: vi.fn().mockResolvedValue('http://127.0.0.1/asset'), saveImageWorkspaceAsset: vi.fn().mockResolvedValue({ status: 'saved' }), uploadImageWorkspaceAsset: vi.fn(), }; }); function prepareWorkspace(overrides = {}) { const workspace = designWorkspaceFixture(overrides); const actions = { load: vi.fn(), connectEvents: vi.fn(), disconnectEvents: vi.fn(), refreshWorkspace: vi.fn().mockResolvedValue(workspace), applyFieldOperations: vi.fn().mockResolvedValue(workspace), sendChat: vi.fn().mockResolvedValue(workspace), resolveDecisionPrompt: vi.fn().mockResolvedValue(workspace), requestQuote: vi.fn().mockResolvedValue(workspace), confirmGeneration: vi.fn().mockResolvedValue(workspace), retryOperation: vi.fn().mockResolvedValue(workspace), }; useImageWorkspaceStore.setState({ status: 'ready', bootstrap: designBootstrapFixture(workspace.workspace), activeWorkspaceId: workspace.workspace.workspaceId, workspace, eventState: 'connected', lastEventId: null, pendingOperations: {}, fieldDrafts: {}, chatDraft: '', assistantStreams: {}, quoteBlockers: [], error: null, ...actions, }); return { workspace, actions }; } describe('Living Form Canvas page', () => { beforeEach(() => { vi.clearAllMocks(); useAuthStore.setState({ initialized: true, accessToken: 'token', expiresAt: Date.now() + 60_000, }); useImagePromptMuseumStore.setState({ pendingPrompt: null }); useImageWorkspaceStore.getState().reset(); }); it('shows the persistent form beside the conversation from the first Workspace snapshot', () => { prepareWorkspace(); render(); expect(screen.getByTestId('image-workspace-living-form')).toBeInTheDocument(); expect(screen.getByText('Living Form')).toBeInTheDocument(); expect(screen.getByText('目标与用途')).toBeInTheDocument(); expect(screen.getByText('内容与叙事')).toBeInTheDocument(); expect(screen.getByText('Agent 建议')).toBeInTheDocument(); expect(screen.getByText('做一张便携咖啡机的小红书主视觉')).toBeInTheDocument(); expect(screen.getByText('编译生成方案')).toBeInTheDocument(); }); it('keeps form edits local until the user saves an explicit field operation', () => { const { actions } = prepareWorkspace(); render(); const ratio = screen.getByDisplayValue('3:4'); fireEvent.change(ratio, { target: { value: '16:9' } }); expect(useImageWorkspaceStore.getState().fieldDrafts).toEqual({ 'output.aspect_ratio': '16:9', }); const field = screen.getByText('画幅比例').closest('.space-y-2'); expect(field).not.toBeNull(); fireEvent.click(within(field as HTMLElement).getByRole('button', { name: '保存' })); expect(actions.applyFieldOperations).toHaveBeenCalledWith( [{ kind: 'set', path: 'output.aspect_ratio', value: '16:9' }], ['output.aspect_ratio'], ); }); it('renders an immutable quote and confirms only its identity', () => { const workspace = designWorkspaceFixture({ form: designFormFixture({ activeQuotes: [designQuoteFixture()] }), }); const { actions } = prepareWorkspace(workspace); render(); expect(screen.getByText('报价已冻结')).toBeInTheDocument(); expect(screen.getByText('12')).toBeInTheDocument(); expect(screen.queryByDisplayValue(/prompt/i)).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '确认生成' })); expect(actions.confirmGeneration).toHaveBeenCalledWith('quote-1'); }); it('prefills a museum Prompt without automatically submitting it', () => { const { actions } = prepareWorkspace(); useImagePromptMuseumStore.setState({ pendingPrompt: { promptId: 'prompt-1', title: '灵感', prompt: '做一张冷色调产品海报' }, }); render(); expect(screen.getByDisplayValue('做一张冷色调产品海报')).toBeInTheDocument(); expect(actions.sendChat).not.toHaveBeenCalled(); }); it('explains and safely retries a command whose accepted result is unknown', () => { const { actions } = prepareWorkspace(); useImageWorkspaceStore.setState({ pendingOperations: { 'operation-1': { id: 'operation-1', label: '编译生成方案', command: { kind: 'request_quote', workspaceId: 'workspace-1', sessionId: 'session-1', expectedDirectionRevision: 4, specificationRevision: 3, clientOperationId: 'operation-1', }, status: 'unknown', error: '网络已断开', clearDraftPaths: [], clearChatDraft: false, }, }, }); render(); expect(screen.getByText('有操作结果尚未确认')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '安全重试' })); expect(actions.retryOperation).toHaveBeenCalledWith('operation-1'); }); it('opens project creation when the account has no Design Workspace', () => { useImageWorkspaceStore.setState({ status: 'ready', bootstrap: { ...designBootstrapFixture(), workspaces: [] }, activeWorkspaceId: null, workspace: null, error: null, load: vi.fn(), }); const listener = vi.fn(); window.addEventListener('niancode:image-workspace:create-project', listener); render(); fireEvent.click(screen.getByRole('button', { name: '新建设计项目' })); expect(listener).toHaveBeenCalledOnce(); window.removeEventListener('niancode:image-workspace:create-project', listener); }); });