173 lines
6.3 KiB
TypeScript
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);
|
|
});
|
|
});
|