190 lines
7.3 KiB
TypeScript
190 lines
7.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 };
|
||
}
|
||
|
||
function openMobileCreationCard() {
|
||
fireEvent.click(screen.getByRole('button', { name: /^我的创作/ }));
|
||
}
|
||
|
||
describe('youth AI Design Canvas page', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
useAuthStore.setState({
|
||
initialized: true,
|
||
accessToken: 'token',
|
||
expiresAt: Date.now() + 60_000,
|
||
});
|
||
useImagePromptMuseumStore.setState({ pendingPrompt: null });
|
||
useImageWorkspaceStore.getState().reset();
|
||
});
|
||
|
||
it('starts with conversation and a concise Creation Card instead of a professional field matrix', () => {
|
||
prepareWorkspace();
|
||
render(<ImageCanvas />);
|
||
|
||
expect(screen.getByTestId('image-workspace-conversation')).toBeInTheDocument();
|
||
expect(screen.getByRole('heading', { name: '一起做作品' })).toBeInTheDocument();
|
||
openMobileCreationCard();
|
||
const card = screen.getByTestId('youth-creation-card');
|
||
expect(card).toBeInTheDocument();
|
||
expect(within(card).getByRole('heading', { name: '我的创作' })).toBeInTheDocument();
|
||
expect(screen.getByText('做一张便携咖啡机的小红书主视觉')).toBeInTheDocument();
|
||
expect(screen.queryByText('Living Form')).not.toBeInTheDocument();
|
||
expect(screen.queryByText('目标与用途')).not.toBeInTheDocument();
|
||
expect(screen.queryByText(/规格版本|编译器|生成策略|字段决策/)).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('sends high-impact fine adjustments through the existing typed field operations', () => {
|
||
const { actions } = prepareWorkspace();
|
||
render(<ImageCanvas />);
|
||
|
||
openMobileCreationCard();
|
||
fireEvent.click(screen.getByRole('button', { name: '精细调整' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '电脑横屏' }));
|
||
|
||
expect(actions.applyFieldOperations).toHaveBeenCalledWith(
|
||
[
|
||
{ kind: 'set', path: 'output.aspect_ratio', value: '16:9' },
|
||
{ kind: 'set', path: 'output.orientation', value: 'landscape' },
|
||
],
|
||
[],
|
||
);
|
||
});
|
||
|
||
it('shows a plain-language production confirmation and confirms only the Quote identity', () => {
|
||
const quote = designQuoteFixture();
|
||
quote.warnings = [{
|
||
code: 'reference_observation_fallback',
|
||
message: 'Reference observations were not reviewed by the frozen compiler.',
|
||
severity: 'warning',
|
||
path: 'references',
|
||
}];
|
||
const workspace = designWorkspaceFixture({
|
||
form: designFormFixture({ activeQuotes: [quote] }),
|
||
});
|
||
const { actions } = prepareWorkspace(workspace);
|
||
render(<ImageCanvas />);
|
||
|
||
openMobileCreationCard();
|
||
expect(screen.getByRole('heading', { name: '制作方案准备好了' })).toBeInTheDocument();
|
||
expect(screen.getByText('12')).toBeInTheDocument();
|
||
expect(screen.getByText('有张参考图还没检查完成,AI 会谨慎使用它。')).toBeInTheDocument();
|
||
expect(screen.queryByText(/frozen compiler/i)).not.toBeInTheDocument();
|
||
expect(screen.queryByDisplayValue(/prompt/i)).not.toBeInTheDocument();
|
||
expect(screen.queryByText(/规格版本|报价已冻结|生成方式|不可变/)).not.toBeInTheDocument();
|
||
fireEvent.click(screen.getByRole('button', { name: '花 12 设计点开始制作' }));
|
||
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 an unknown accepted result without suggesting a second production', () => {
|
||
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 />);
|
||
|
||
openMobileCreationCard();
|
||
expect(screen.getByText('还在确认刚才的操作')).toBeInTheDocument();
|
||
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);
|
||
});
|
||
});
|