Files
makelore/tests/unit/image-canvas-page.test.tsx

173 lines
6.3 KiB
TypeScript

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<typeof import('@/lib/image-workspace')>();
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(<ImageCanvas />);
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(<ImageCanvas />);
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(<ImageCanvas />);
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(<ImageCanvas />);
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(<ImageCanvas />);
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(<ImageCanvas />);
fireEvent.click(screen.getByRole('button', { name: '新建设计项目' }));
expect(listener).toHaveBeenCalledOnce();
window.removeEventListener('niancode:image-workspace:create-project', listener);
});
});