merge: integrate remote main
This commit is contained in:
@@ -22,12 +22,25 @@ 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'),
|
||||
resolveImageWorkspaceAssetUrl: vi.fn().mockReturnValue(new Promise(() => undefined)),
|
||||
saveImageWorkspaceAsset: vi.fn().mockResolvedValue({ status: 'saved' }),
|
||||
uploadImageWorkspaceAsset: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
function setViewport(desktop: boolean) {
|
||||
window.matchMedia = vi.fn().mockImplementation(() => ({
|
||||
matches: desktop,
|
||||
media: '(min-width: 1024px)',
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
function prepareWorkspace(overrides = {}) {
|
||||
const workspace = designWorkspaceFixture(overrides);
|
||||
const actions = {
|
||||
@@ -35,6 +48,10 @@ function prepareWorkspace(overrides = {}) {
|
||||
connectEvents: vi.fn(),
|
||||
disconnectEvents: vi.fn(),
|
||||
refreshWorkspace: vi.fn().mockResolvedValue(workspace),
|
||||
createProject: vi.fn().mockResolvedValue(workspace),
|
||||
renameProject: vi.fn().mockResolvedValue(workspace),
|
||||
deleteProject: vi.fn().mockResolvedValue({ workspaceId: 'workspace-1', deleted: true }),
|
||||
selectProject: vi.fn().mockResolvedValue(undefined),
|
||||
applyFieldOperations: vi.fn().mockResolvedValue(workspace),
|
||||
sendChat: vi.fn().mockResolvedValue(workspace),
|
||||
resolveDecisionPrompt: vi.fn().mockResolvedValue(workspace),
|
||||
@@ -61,36 +78,51 @@ function prepareWorkspace(overrides = {}) {
|
||||
return { workspace, actions };
|
||||
}
|
||||
|
||||
function openMobileCreationCard() {
|
||||
fireEvent.click(screen.getByRole('button', { name: /^当前想法与作品/ }));
|
||||
}
|
||||
|
||||
describe('youth AI Design Canvas page', () => {
|
||||
describe('AI Design Canvas page', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setViewport(false);
|
||||
useAuthStore.setState({
|
||||
initialized: true,
|
||||
accessToken: 'token',
|
||||
expiresAt: Date.now() + 60_000,
|
||||
lastActiveAt: Date.now(),
|
||||
canRefresh: true,
|
||||
user: {
|
||||
username: 'canvas-tester',
|
||||
userId: 'canvas-user',
|
||||
tenantId: null,
|
||||
deptId: null,
|
||||
authorities: [],
|
||||
},
|
||||
});
|
||||
useImagePromptMuseumStore.setState({ pendingPrompt: null });
|
||||
useImageWorkspaceStore.getState().reset();
|
||||
});
|
||||
|
||||
it('starts with a guided conversation and keeps the structured summary secondary', () => {
|
||||
it('keeps the loading shell until the first Workspace bootstrap settles', () => {
|
||||
const load = vi.fn();
|
||||
useImageWorkspaceStore.setState({ status: 'idle', workspace: null, bootstrap: null, load });
|
||||
|
||||
render(<ImageCanvas />);
|
||||
|
||||
expect(screen.getByLabelText('正在加载 AI 设计')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('image-workspace-empty-state')).not.toBeInTheDocument();
|
||||
expect(load).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('keeps conversation and the active editable plan together in the center', () => {
|
||||
prepareWorkspace();
|
||||
render(<ImageCanvas />);
|
||||
|
||||
expect(screen.getByTestId('image-workspace-conversation')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '和 AI 聊聊你的想法' })).toBeInTheDocument();
|
||||
expect(screen.getByText('像和同学讲故事一样说就好。AI 会先听懂,再一次问一个最重要的问题。')).toBeInTheDocument();
|
||||
openMobileCreationCard();
|
||||
const card = screen.getByTestId('youth-creation-card');
|
||||
expect(card).toBeInTheDocument();
|
||||
expect(within(card).getByRole('heading', { name: 'AI 听懂的想法' })).toBeInTheDocument();
|
||||
expect(screen.getByText('做一张便携咖啡机的小红书主视觉')).toBeInTheDocument();
|
||||
const conversation = screen.getByTestId('image-workspace-conversation');
|
||||
expect(within(conversation).getByRole('heading', { name: '和 AI 一起完善创作' })).toBeInTheDocument();
|
||||
expect(within(conversation).getByRole('heading', { name: '制作方案' })).toBeInTheDocument();
|
||||
expect(within(conversation).getByRole('textbox', { name: '创作提示词' })).toHaveValue(
|
||||
'把便携咖啡机呈现为城市通勤中的精密工具',
|
||||
);
|
||||
expect(within(conversation).getByRole('combobox', { name: '画幅' })).toHaveValue('3:4');
|
||||
expect(screen.queryByText('Living Form')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('目标与用途')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/规格版本|编译器|生成策略|字段决策/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -125,6 +157,39 @@ describe('youth AI Design Canvas page', () => {
|
||||
expect(screen.getByRole('button', { name: '发送消息' })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('renders the Workspace list as a full-height right Works rail on desktop', () => {
|
||||
setViewport(true);
|
||||
prepareWorkspace();
|
||||
render(<ImageCanvas />);
|
||||
|
||||
const rail = screen.getByTestId('image-workspace-works-rail');
|
||||
expect(rail).toBeInTheDocument();
|
||||
expect(within(rail).getByRole('heading', { name: '我的作品' })).toBeInTheDocument();
|
||||
expect(within(rail).getByRole('button', { name: '打开作品 咖啡机新品主视觉' })).toBeInTheDocument();
|
||||
expect(within(rail).getByRole('button', { name: '新建作品' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '作品' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('prioritizes an offered current plan over older completed-work status', () => {
|
||||
setViewport(true);
|
||||
prepareWorkspace({
|
||||
form: designFormFixture({ activeQuotes: [designQuoteFixture()] }),
|
||||
});
|
||||
render(<ImageCanvas />);
|
||||
|
||||
expect(screen.getByText(/制作方案已准备好,确认前仍可修改/)).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId('image-workspace-works-rail')).getByText('待确认')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the Works rail as a right sheet on compact layouts', () => {
|
||||
prepareWorkspace();
|
||||
render(<ImageCanvas />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '作品' }));
|
||||
expect(screen.getByTestId('image-workspace-works-rail')).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId('image-workspace-works-rail')).getByRole('heading', { name: '我的作品' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows one unfinished assistant reply incrementally and replaces it with the canonical turn', async () => {
|
||||
const { workspace } = prepareWorkspace();
|
||||
const firstChunk = '收到,我们要制作';
|
||||
@@ -153,26 +218,15 @@ describe('youth AI Design Canvas page', () => {
|
||||
});
|
||||
|
||||
render(<ImageCanvas />);
|
||||
|
||||
const conversation = screen.getByTestId('image-workspace-conversation');
|
||||
const pendingMessage = within(conversation).getByTestId('pending-design-chat-operation-chat-1');
|
||||
expect(within(pendingMessage).getByText('做一张社团活动海报')).toBeInTheDocument();
|
||||
expect(within(pendingMessage).getByText('发送中')).toBeInTheDocument();
|
||||
expect(within(conversation).queryByRole('button', { name: '下一步:看看制作方案' }))
|
||||
.not.toBeInTheDocument();
|
||||
expect(within(conversation).queryByTestId('design-assistant-progress')).not.toBeInTheDocument();
|
||||
expect(within(conversation).queryByText('AI 正在整理你的想法')).not.toBeInTheDocument();
|
||||
expect(within(conversation).getByText('我已经整理了用途、受众和初步概念,请确认右侧建议。')).toBeInTheDocument();
|
||||
const streamingReply = within(conversation)
|
||||
.getByTestId('streaming-design-assistant-operation-chat-1');
|
||||
expect(streamingReply).toHaveTextContent(firstChunk);
|
||||
expect(within(conversation).getByTestId('pending-design-chat-operation-chat-1')).toHaveTextContent('发送中');
|
||||
expect(within(conversation).getByTestId('streaming-design-assistant-operation-chat-1')).toHaveTextContent(firstChunk);
|
||||
expect(within(conversation).queryByTestId('youth-creation-card')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
useImageWorkspaceStore.setState({
|
||||
assistantStreams: { 'operation-chat-1': unfinishedDraft },
|
||||
});
|
||||
useImageWorkspaceStore.setState({ assistantStreams: { 'operation-chat-1': unfinishedDraft } });
|
||||
});
|
||||
expect(streamingReply).toHaveTextContent(unfinishedDraft);
|
||||
expect(within(conversation).getByTestId('streaming-design-assistant-operation-chat-1')).toHaveTextContent(unfinishedDraft);
|
||||
|
||||
act(() => {
|
||||
useImageWorkspaceStore.setState({
|
||||
@@ -190,12 +244,10 @@ describe('youth AI Design Canvas page', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(within(conversation).queryByTestId('streaming-design-assistant-operation-chat-1'))
|
||||
.not.toBeInTheDocument();
|
||||
expect(within(conversation).queryByTestId('streaming-design-assistant-operation-chat-1')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(within(conversation).getAllByText(unfinishedDraft)).toHaveLength(1);
|
||||
expect(within(conversation).getByRole('button', { name: '下一步:看看制作方案' }))
|
||||
.toBeInTheDocument();
|
||||
expect(within(conversation).getByTestId('youth-creation-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps each pending reply beside the user message that started it', () => {
|
||||
@@ -344,7 +396,7 @@ describe('youth AI Design Canvas page', () => {
|
||||
expect(within(completedActivity).getByText('正在听懂你刚补充的内容…')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the mobile quote confirmation from the ready conversation without starting a paid task', async () => {
|
||||
it('requests a Quote from the inline plan without starting a paid task', async () => {
|
||||
const { actions } = prepareWorkspace();
|
||||
const quotedWorkspace = designWorkspaceFixture({
|
||||
form: designFormFixture({ activeQuotes: [designQuoteFixture()] }),
|
||||
@@ -355,15 +407,14 @@ describe('youth AI Design Canvas page', () => {
|
||||
});
|
||||
render(<ImageCanvas />);
|
||||
|
||||
const conversation = screen.getByTestId('image-workspace-conversation');
|
||||
fireEvent.click(within(conversation).getByRole('button', { name: '下一步:看看制作方案' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '准备制作方案' }));
|
||||
|
||||
expect(actions.requestQuote).toHaveBeenCalledOnce();
|
||||
expect(actions.confirmGeneration).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading', { name: '制作方案准备好了' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '确认并开始制作' })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('design-offered-quote')).toBeInTheDocument();
|
||||
expect(screen.getByText(/预计使用/)).toHaveTextContent('12');
|
||||
});
|
||||
|
||||
it('shows a submitted message immediately and replaces it with the canonical turn', async () => {
|
||||
@@ -391,32 +442,11 @@ describe('youth AI Design Canvas page', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ImageCanvas />);
|
||||
|
||||
const conversation = screen.getByTestId('image-workspace-conversation');
|
||||
const pendingMessage = within(conversation).getByTestId('pending-design-chat-operation-chat-1');
|
||||
expect(within(pendingMessage).getByText(message)).toBeInTheDocument();
|
||||
expect(within(pendingMessage).getByText('发送中')).toBeInTheDocument();
|
||||
expect(within(conversation).queryByRole('heading', { name: '先把脑中的画面说出来' }))
|
||||
.not.toBeInTheDocument();
|
||||
expect(within(conversation).getByRole('textbox', { name: '告诉 AI 你想创作什么' }))
|
||||
.toHaveValue('');
|
||||
|
||||
act(() => {
|
||||
useImageWorkspaceStore.setState((state) => ({
|
||||
pendingOperations: {
|
||||
...state.pendingOperations,
|
||||
'operation-chat-1': {
|
||||
...state.pendingOperations['operation-chat-1']!,
|
||||
status: 'unknown',
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
expect(within(pendingMessage).getByText('正在确认')).toBeInTheDocument();
|
||||
expect(within(conversation).getByRole('textbox', { name: '告诉 AI 你想创作什么' }))
|
||||
.toHaveValue('');
|
||||
expect(within(conversation).getByTestId('pending-design-chat-operation-chat-1')).toHaveTextContent(message);
|
||||
expect(within(conversation).getByRole('textbox', { name: '告诉 AI 你想创作什么' })).toHaveValue('');
|
||||
|
||||
act(() => {
|
||||
useImageWorkspaceStore.setState({
|
||||
@@ -434,13 +464,8 @@ describe('youth AI Design Canvas page', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(within(conversation).getAllByText(message)).toHaveLength(1);
|
||||
});
|
||||
expect(within(conversation).queryByTestId('pending-design-chat-operation-chat-1'))
|
||||
.not.toBeInTheDocument();
|
||||
expect(within(conversation).queryByText('发送中')).not.toBeInTheDocument();
|
||||
expect(within(conversation).getByText(/更像漫画,还是更像电影/)).toBeInTheDocument();
|
||||
await waitFor(() => expect(within(conversation).getAllByText(message)).toHaveLength(1));
|
||||
expect(within(conversation).queryByTestId('pending-design-chat-operation-chat-1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('invites a free description and does not turn a legacy decision prompt into a form', () => {
|
||||
@@ -453,10 +478,7 @@ describe('youth AI Design Canvas page', () => {
|
||||
targetPaths: ['visual.art_direction'],
|
||||
title: '画面想要什么感觉?',
|
||||
body: '请从下面选一个。',
|
||||
options: [
|
||||
{ id: 'cartoon', label: '有趣的卡通', description: null },
|
||||
{ id: 'cinema', label: '像电影一样', description: null },
|
||||
],
|
||||
options: [{ id: 'cartoon', label: '有趣的卡通', description: null }],
|
||||
}],
|
||||
}),
|
||||
turns: [],
|
||||
@@ -466,34 +488,28 @@ describe('youth AI Design Canvas page', () => {
|
||||
|
||||
const conversation = screen.getByTestId('image-workspace-conversation');
|
||||
expect(within(conversation).getByRole('heading', { name: '先把脑中的画面说出来' })).toBeInTheDocument();
|
||||
expect(within(conversation).getByText(/想画什么、发生在哪里/)).toBeInTheDocument();
|
||||
expect(within(conversation).queryByText('画面想要什么感觉?')).not.toBeInTheDocument();
|
||||
expect(within(conversation).queryByRole('button', { name: '有趣的卡通' })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(conversation).getByRole('button', { name: '我想做一张热闹的社团招新海报' }));
|
||||
expect(within(conversation).getByRole('textbox', { name: '告诉 AI 你想创作什么' })).toHaveValue(
|
||||
'我想做一张热闹的社团招新海报',
|
||||
);
|
||||
});
|
||||
|
||||
it('sends high-impact fine adjustments through the existing typed field operations', () => {
|
||||
it('sends compact production edits through the existing typed field operations', async () => {
|
||||
const { actions } = prepareWorkspace();
|
||||
render(<ImageCanvas />);
|
||||
|
||||
openMobileCreationCard();
|
||||
fireEvent.click(screen.getByRole('button', { name: '手动调整(可选)' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '电脑横屏' }));
|
||||
fireEvent.change(screen.getByRole('combobox', { name: '画幅' }), { target: { value: '16:9' } });
|
||||
|
||||
expect(actions.applyFieldOperations).toHaveBeenCalledWith(
|
||||
[
|
||||
await waitFor(() => {
|
||||
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', () => {
|
||||
it('confirms only the offered Quote identity and keeps provider details private', () => {
|
||||
const quote = designQuoteFixture();
|
||||
quote.warnings = [{
|
||||
code: 'reference_observation_fallback',
|
||||
@@ -501,20 +517,14 @@ describe('youth AI Design Canvas page', () => {
|
||||
severity: 'warning',
|
||||
path: 'references',
|
||||
}];
|
||||
const workspace = designWorkspaceFixture({
|
||||
const { actions } = prepareWorkspace({
|
||||
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 设计点开始制作' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认并开始制作' }));
|
||||
expect(actions.confirmGeneration).toHaveBeenCalledWith('quote-1');
|
||||
});
|
||||
|
||||
@@ -548,10 +558,9 @@ describe('youth AI Design Canvas page', () => {
|
||||
'AI 这次没听懂。你的内容还在输入框里,可以补充一个画面细节后再发送',
|
||||
);
|
||||
});
|
||||
expect(toastErrorMock).not.toHaveBeenCalledWith('消息没有发出去,请再试一次');
|
||||
});
|
||||
|
||||
it('explains an unknown accepted result without suggesting a second production', () => {
|
||||
it('keeps an unknown accepted operation recoverable without offering a duplicate action', () => {
|
||||
const { actions } = prepareWorkspace();
|
||||
useImageWorkspaceStore.setState({
|
||||
pendingOperations: {
|
||||
@@ -576,18 +585,14 @@ describe('youth AI Design Canvas page', () => {
|
||||
});
|
||||
render(<ImageCanvas />);
|
||||
|
||||
expect(within(screen.getByTestId('image-workspace-conversation'))
|
||||
.getByRole('button', { name: '正在准备方案' })).toBeDisabled();
|
||||
openMobileCreationCard();
|
||||
expect(screen.getByText('还在确认刚才的操作')).toBeInTheDocument();
|
||||
expect(screen.getByText(/不会重复制作或重复扣费/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '看看制作方案', exact: true })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: '准备制作方案' })).toBeDisabled();
|
||||
expect(actions.requestQuote).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看原来的结果' }));
|
||||
expect(actions.retryOperation).toHaveBeenCalledWith('operation-1');
|
||||
});
|
||||
|
||||
it('opens project creation when the account has no Design Workspace', () => {
|
||||
it('opens project creation from an empty Canvas', async () => {
|
||||
useImageWorkspaceStore.setState({
|
||||
status: 'ready',
|
||||
bootstrap: { ...designBootstrapFixture(), workspaces: [] },
|
||||
@@ -595,13 +600,11 @@ describe('youth AI Design Canvas page', () => {
|
||||
workspace: null,
|
||||
error: null,
|
||||
load: vi.fn(),
|
||||
createProject: 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);
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始第一个作品' }));
|
||||
await waitFor(() => expect(screen.getByRole('heading', { name: '新建作品' })).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user