feat: 完善图像工作区与创作工具体验
This commit is contained in:
@@ -4,10 +4,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
||||
import { ImageCanvas } from '@/pages/ImageCanvas';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useImagePromptMuseumStore } from '@/stores/image-prompt-museum';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import type {
|
||||
DesignAssistantDeltaEvent,
|
||||
DesignConversation,
|
||||
DesignGenerationQuote,
|
||||
DesignGenerationTask,
|
||||
DesignGenerationTaskUpdatedEvent,
|
||||
DesignWorkspace,
|
||||
@@ -20,6 +22,7 @@ const fetchImageWorkspaceConversationMock = vi.hoisted(() => vi.fn());
|
||||
const fetchImageWorkspaceTasksMock = vi.hoisted(() => vi.fn());
|
||||
const sendImageWorkspaceMessageMock = vi.hoisted(() => vi.fn());
|
||||
const confirmImageWorkspaceGenerationMock = vi.hoisted(() => vi.fn());
|
||||
const updateImageWorkspaceGenerationQuoteMock = vi.hoisted(() => vi.fn());
|
||||
const openImageWorkspaceTaskEventsMock = vi.hoisted(() => vi.fn());
|
||||
const resolveImageWorkspaceAssetUrlMock = vi.hoisted(() => vi.fn());
|
||||
const saveImageWorkspaceAssetMock = vi.hoisted(() => vi.fn());
|
||||
@@ -67,6 +70,9 @@ vi.mock('@/lib/image-workspace', async (importOriginal) => {
|
||||
confirmImageWorkspaceGeneration: (...args: unknown[]) => (
|
||||
confirmImageWorkspaceGenerationMock(...args)
|
||||
),
|
||||
updateImageWorkspaceGenerationQuote: (...args: unknown[]) => (
|
||||
updateImageWorkspaceGenerationQuoteMock(...args)
|
||||
),
|
||||
openImageWorkspaceTaskEvents: (...args: unknown[]) => (
|
||||
openImageWorkspaceTaskEventsMock(...args)
|
||||
),
|
||||
@@ -139,6 +145,55 @@ function conversationFixture(): DesignConversation {
|
||||
medium: 'image',
|
||||
briefVersion: 1,
|
||||
briefSummary: '夜色中的机械城堡角色海报',
|
||||
finalPrompt: '夜色中的机械城堡角色海报,电影感构图。',
|
||||
promptMode: 'guided',
|
||||
generationParameters: {
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
aspectRatio: '1:1',
|
||||
durationSeconds: null,
|
||||
},
|
||||
generationOptions: {
|
||||
models: [{
|
||||
value: 'image-model-one',
|
||||
label: '图片模型一',
|
||||
default: true,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}],
|
||||
resolutions: [{
|
||||
value: '1024x1024',
|
||||
label: '1024 × 1024',
|
||||
default: true,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}, {
|
||||
value: '2048x2048',
|
||||
label: '2048 × 2048',
|
||||
default: false,
|
||||
disabled: false,
|
||||
multiplier: 1.5,
|
||||
}],
|
||||
durations: [],
|
||||
aspectRatios: [{
|
||||
value: '1:1',
|
||||
label: '1:1',
|
||||
default: true,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}, {
|
||||
value: '16:9',
|
||||
label: '16:9',
|
||||
default: false,
|
||||
disabled: false,
|
||||
multiplier: 1,
|
||||
}],
|
||||
},
|
||||
pricing: {
|
||||
schema: 'design-pricing-v2',
|
||||
amount: 1,
|
||||
rounding: 'ceil',
|
||||
},
|
||||
quotedDesignPoints: 1,
|
||||
expiresAt: '2026-07-31T11:00:00Z',
|
||||
},
|
||||
@@ -189,11 +244,28 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useImageWorkspaceStore.getState().reset();
|
||||
useImagePromptMuseumStore.getState().clearPendingPrompt();
|
||||
fetchImageWorkspaceMock.mockResolvedValue(bootstrapFixture);
|
||||
fetchImageWorkspaceProjectMock.mockResolvedValue(workspaceFixture());
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValue(conversationFixture());
|
||||
fetchImageWorkspaceTasksMock.mockResolvedValue([taskFixture]);
|
||||
sendImageWorkspaceMessageMock.mockResolvedValue(conversationFixture());
|
||||
updateImageWorkspaceGenerationQuoteMock.mockImplementation(async (
|
||||
_workspaceId: string,
|
||||
_quoteId: string,
|
||||
finalPrompt: string,
|
||||
generationParameters: DesignGenerationQuote['generationParameters'],
|
||||
) => ({
|
||||
...conversationFixture().messages[1].generationQuote!,
|
||||
finalPrompt,
|
||||
generationParameters,
|
||||
quotedDesignPoints: 3,
|
||||
pricing: {
|
||||
schema: 'design-pricing-v2',
|
||||
amount: 3,
|
||||
rounding: 'ceil',
|
||||
},
|
||||
}));
|
||||
confirmImageWorkspaceGenerationMock.mockResolvedValue({
|
||||
...conversationFixture(),
|
||||
turnRevision: 2,
|
||||
@@ -301,13 +373,115 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
expect(screen.getByTestId('design-quote-quote-one')).toHaveTextContent('1 设计点');
|
||||
expect(screen.getByTestId('design-task-list')).toHaveTextContent('生成内容');
|
||||
expect(screen.queryByText('图片与视频任务统一展示')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('design-task-task-one')).toHaveTextContent('已完成');
|
||||
await waitFor(() => expect(screen.getByRole('img', { name: 'AI 设计生成结果' }))
|
||||
const taskCard = screen.getByTestId('design-task-task-one');
|
||||
expect(taskCard).toHaveTextContent('生图');
|
||||
expect(taskCard.querySelector('time')).toHaveAttribute('datetime', taskFixture.createdAt);
|
||||
expect(taskCard).not.toHaveTextContent('已完成');
|
||||
expect(screen.queryByRole('button', { name: '下载原图' })).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByRole('img', { name: 'AI 设计生成缩略图' }))
|
||||
.toHaveAttribute('src', 'http://127.0.0.1:13210/content?token=host'));
|
||||
expect(screen.queryByText('生成设置')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/当前 Agent/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps task cards fixed-size and truncates long descriptions', async () => {
|
||||
const longDescription = '夜色中的机械城堡角色海报,加入冷暖对比和电影感轮廓光效果';
|
||||
const compactDescription = `${Array.from(longDescription).slice(0, 24).join('')}…`;
|
||||
fetchImageWorkspaceTasksMock.mockResolvedValueOnce([{
|
||||
...taskFixture,
|
||||
taskId: 'task-long-description',
|
||||
briefSummary: longDescription,
|
||||
}]);
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
const taskCard = await screen.findByTestId('design-task-task-long-description');
|
||||
expect(taskCard).toHaveClass('h-20', 'w-full');
|
||||
expect(within(taskCard).getByRole('button')).toHaveClass('h-full');
|
||||
expect(taskCard).toHaveTextContent(compactDescription);
|
||||
expect(taskCard).not.toHaveTextContent(longDescription);
|
||||
});
|
||||
|
||||
it('edits the final prompt and server-provided options before re-quoting', async () => {
|
||||
const requote = deferred<DesignGenerationQuote>();
|
||||
updateImageWorkspaceGenerationQuoteMock.mockReset();
|
||||
updateImageWorkspaceGenerationQuoteMock.mockReturnValueOnce(requote.promise);
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
const finalPrompt = await screen.findByTestId('design-quote-final-prompt');
|
||||
fireEvent.change(finalPrompt, { target: { value: '用户最新编辑后的完整提示词' } });
|
||||
fireEvent.change(screen.getByTestId('design-quote-resolution'), {
|
||||
target: { value: '2048x2048' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId('design-quote-aspect-ratio'), {
|
||||
target: { value: '16:9' },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalledWith(
|
||||
'workspace-cloud',
|
||||
'quote-one',
|
||||
'用户最新编辑后的完整提示词',
|
||||
{
|
||||
model: 'image-model-one',
|
||||
resolution: '2048x2048',
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds: null,
|
||||
},
|
||||
));
|
||||
expect(screen.getByRole('button', { name: '重新报价中' })).toBeDisabled();
|
||||
|
||||
requote.resolve({
|
||||
...conversationFixture().messages[1].generationQuote!,
|
||||
finalPrompt: '用户最新编辑后的完整提示词',
|
||||
generationParameters: {
|
||||
model: 'image-model-one',
|
||||
resolution: '2048x2048',
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds: null,
|
||||
},
|
||||
quotedDesignPoints: 3,
|
||||
pricing: { schema: 'design-pricing-v2', amount: 3, rounding: 'ceil' },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('design-quote-quote-one'))
|
||||
.toHaveTextContent('3 设计点'));
|
||||
expect(screen.getByTestId('design-quote-final-prompt'))
|
||||
.toHaveValue('用户最新编辑后的完整提示词');
|
||||
const quoteCard = screen.getByTestId('design-quote-quote-one');
|
||||
expect(quoteCard).toHaveTextContent('报价 3 设计点');
|
||||
expect(quoteCard).toHaveTextContent('继续调整');
|
||||
expect(quoteCard).toHaveTextContent('清晰度');
|
||||
expect(quoteCard).toHaveTextContent('画幅');
|
||||
expect(quoteCard).not.toHaveTextContent('提示词模式:guided');
|
||||
expect(quoteCard).not.toHaveTextContent('夜色中的机械城堡角色海报');
|
||||
expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('offers an actionable retry when re-quoting fails', async () => {
|
||||
updateImageWorkspaceGenerationQuoteMock.mockRejectedValueOnce(
|
||||
new ImageWorkspaceApiError(409, 'generation_quote_invalid', '当前生成方案已失效'),
|
||||
);
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
fireEvent.change(await screen.findByTestId('design-quote-final-prompt'), {
|
||||
target: { value: '需要重新报价的提示词' },
|
||||
});
|
||||
|
||||
const quoteCard = screen.getByTestId('design-quote-quote-one');
|
||||
await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalled());
|
||||
expect(await within(quoteCard).findByRole('alert')).toHaveTextContent('当前生成方案已失效');
|
||||
expect(within(quoteCard).getByText(/请点击“重试报价”/)).toBeInTheDocument();
|
||||
expect(within(quoteCard).getByRole('button', { name: '重试报价' })).toBeEnabled();
|
||||
expect(within(quoteCard).getByRole('button', { name: '继续调整' })).toBeEnabled();
|
||||
expect(within(quoteCard).getByRole('button', { name: '等待报价更新' })).toBeDisabled();
|
||||
|
||||
fireEvent.click(within(quoteCard).getByRole('button', { name: '重试报价' }));
|
||||
await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeEnabled());
|
||||
});
|
||||
|
||||
it('uses the compact logo-only empty state for a new design conversation', async () => {
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce({
|
||||
...conversationFixture(),
|
||||
@@ -324,6 +498,62 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
expect(emptyState).not.toHaveTextContent('告诉设计 Agent');
|
||||
});
|
||||
|
||||
it('prefills the exact Prompt Museum text without sending it automatically', async () => {
|
||||
const prompt = 'Create a quiet editorial poster with generous negative space.';
|
||||
useImagePromptMuseumStore.getState().setPendingPrompt({
|
||||
promptId: 'prompt-museum-one',
|
||||
title: '编辑感产品海报',
|
||||
prompt,
|
||||
});
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
const composer = await screen.findByLabelText('设计需求');
|
||||
await waitFor(() => expect(composer).toHaveValue(prompt));
|
||||
expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
|
||||
expect(useImagePromptMuseumStore.getState().pendingPrompt).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps historical choices fixed after a later assistant turn', async () => {
|
||||
const conversation = conversationFixture();
|
||||
conversation.messages[1] = {
|
||||
...conversation.messages[1],
|
||||
quickReplies: ['暖色调', '冷色调'],
|
||||
generationQuote: null,
|
||||
};
|
||||
conversation.messages.push(
|
||||
{
|
||||
id: 'message-follow-up-user',
|
||||
role: 'user',
|
||||
kind: 'user',
|
||||
text: '继续调整色彩',
|
||||
quickReplies: [],
|
||||
generationQuote: null,
|
||||
turnRevision: 2,
|
||||
createdAt: '2026-07-31T10:01:00Z',
|
||||
},
|
||||
{
|
||||
id: 'message-follow-up-assistant',
|
||||
role: 'assistant',
|
||||
kind: 'choice',
|
||||
text: '请选择下一步方向。',
|
||||
quickReplies: ['增加对比度', '保持当前方案'],
|
||||
generationQuote: null,
|
||||
turnRevision: 2,
|
||||
createdAt: '2026-07-31T10:01:01Z',
|
||||
},
|
||||
);
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce(conversation);
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
await screen.findByTestId('image-workspace-conversation');
|
||||
expect(screen.queryByRole('button', { name: '暖色调' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '冷色调' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '增加对比度' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '保持当前方案' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(['consumed', 'expired', 'superseded'] as const)(
|
||||
'hides quote quick replies when the quote is %s',
|
||||
async (status) => {
|
||||
@@ -364,20 +594,50 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
it('opens a generated image at full size and saves it through Main', async () => {
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '放大查看生成图片' }));
|
||||
const taskCard = await screen.findByTestId('design-task-task-one');
|
||||
fireEvent.click(within(taskCard).getByRole('button', { name: '查看生图任务详情' }));
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '图片预览' });
|
||||
const dialog = await screen.findByRole('dialog', { name: '生成内容详情' });
|
||||
expect(dialog).toHaveTextContent('1024 × 1024');
|
||||
expect(screen.getByRole('img', { name: 'AI 设计大图预览' }))
|
||||
expect(within(dialog).getByRole('img', { name: 'AI 设计大图预览' }))
|
||||
.toHaveAttribute('src', 'http://127.0.0.1:13210/content?token=host');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载原图' }));
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '下载原图' }));
|
||||
|
||||
await waitFor(() => expect(saveImageWorkspaceAssetMock).toHaveBeenCalledWith(
|
||||
taskFixture.resultAssets[0],
|
||||
));
|
||||
});
|
||||
|
||||
it('shows video tasks as compact cards and exposes video controls in details', async () => {
|
||||
const videoAsset = {
|
||||
...taskFixture.resultAssets[0],
|
||||
assetId: 'video-asset-one',
|
||||
mediaType: 'video' as const,
|
||||
mimeType: 'video/mp4',
|
||||
durationMilliseconds: 4_000,
|
||||
};
|
||||
const videoTask = {
|
||||
...taskFixture,
|
||||
taskId: 'task-video-one',
|
||||
medium: 'video' as const,
|
||||
resultAssets: [videoAsset],
|
||||
};
|
||||
fetchImageWorkspaceTasksMock.mockResolvedValueOnce([videoTask]);
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
const taskCard = await screen.findByTestId('design-task-task-video-one');
|
||||
expect(taskCard).toHaveTextContent('视频');
|
||||
fireEvent.click(within(taskCard).getByRole('button', { name: '查看视频任务详情' }));
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: '生成内容详情' });
|
||||
expect(within(dialog).getByLabelText('AI 设计视频预览')).toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '下载视频' }));
|
||||
|
||||
await waitFor(() => expect(saveImageWorkspaceAssetMock).toHaveBeenCalledWith(videoAsset));
|
||||
});
|
||||
|
||||
it('announces the image download progress while Main is saving', async () => {
|
||||
let finishSaving: ((value: { status: 'saved' }) => void) | undefined;
|
||||
saveImageWorkspaceAssetMock.mockReturnValue(new Promise((resolve) => {
|
||||
@@ -385,15 +645,18 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
}));
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '下载生成图片' }));
|
||||
const taskCard = await screen.findByTestId('design-task-task-one');
|
||||
fireEvent.click(within(taskCard).getByRole('button', { name: '查看生图任务详情' }));
|
||||
const dialog = await screen.findByRole('dialog', { name: '生成内容详情' });
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '下载原图' }));
|
||||
|
||||
const savingButton = screen.getByRole('button', { name: '正在保存生成图片' });
|
||||
const savingButton = within(dialog).getByRole('button', { name: '正在下载生图' });
|
||||
expect(savingButton).toBeDisabled();
|
||||
expect(savingButton).toHaveAttribute('aria-busy', 'true');
|
||||
|
||||
finishSaving?.({ status: 'saved' });
|
||||
await waitFor(() => expect(
|
||||
screen.getByRole('button', { name: '下载生成图片' }),
|
||||
within(dialog).getByRole('button', { name: '下载原图' }),
|
||||
).toBeEnabled());
|
||||
});
|
||||
|
||||
@@ -620,6 +883,52 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
await waitFor(() => expect(useImageWorkspaceStore.getState().pendingTurn).toBeNull());
|
||||
});
|
||||
|
||||
it('reconciles a running Agent turn after returning to the conversation page', async () => {
|
||||
const response = deferred<DesignConversation>();
|
||||
const canonical: DesignConversation = {
|
||||
...conversationFixture(),
|
||||
turnRevision: 2,
|
||||
phase: 'shaping',
|
||||
messages: [{
|
||||
id: 'message-latest',
|
||||
role: 'assistant',
|
||||
kind: 'reply',
|
||||
text: '已完成最新方向整理。',
|
||||
quickReplies: [],
|
||||
generationQuote: null,
|
||||
turnRevision: 2,
|
||||
createdAt: '2026-07-31T10:01:00Z',
|
||||
}],
|
||||
};
|
||||
sendImageWorkspaceMessageMock.mockReturnValueOnce(response.promise);
|
||||
|
||||
const firstRender = render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
await screen.findByTestId('image-workspace-conversation');
|
||||
await waitFor(() => expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce());
|
||||
await waitFor(() => expect(taskEventSource.onopen).not.toBeNull());
|
||||
act(() => taskEventSource.onopen?.(new Event('open')));
|
||||
|
||||
fireEvent.change(screen.getByLabelText('设计需求'), {
|
||||
target: { value: '继续优化海报' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
|
||||
expect(await screen.findByText('继续优化海报')).toBeInTheDocument();
|
||||
|
||||
firstRender.unmount();
|
||||
fetchImageWorkspaceConversationMock.mockResolvedValueOnce(canonical);
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
expect(await screen.findByText('继续优化海报')).toBeInTheDocument();
|
||||
expect(await screen.findByText('已完成最新方向整理。')).toBeInTheDocument();
|
||||
expect(openImageWorkspaceTaskEventsMock).toHaveBeenCalledOnce();
|
||||
expect(taskEventSource.close).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
response.resolve(canonical);
|
||||
await response.promise;
|
||||
});
|
||||
});
|
||||
|
||||
it('does not expose confirmation quick replies when no active Quote exists', async () => {
|
||||
const workspaceWithoutQuote = conversationFixture();
|
||||
workspaceWithoutQuote.messages[1] = {
|
||||
@@ -676,11 +985,53 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
1,
|
||||
'quote-one',
|
||||
expect.stringMatching(/^turn-/),
|
||||
'夜色中的机械城堡角色海报,电影感构图。',
|
||||
{
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
aspectRatio: '1:1',
|
||||
durationSeconds: null,
|
||||
},
|
||||
));
|
||||
await waitFor(() => expect(fetchImageWorkspaceTasksMock).toHaveBeenCalledTimes(2));
|
||||
expect(await screen.findByTestId('design-task-task-two')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the final confirmation prompt visible in the conversation', async () => {
|
||||
const confirmedConversation: DesignConversation = {
|
||||
...conversationFixture(),
|
||||
turnRevision: 2,
|
||||
phase: 'shaping',
|
||||
messages: [{
|
||||
id: 'message-confirmed',
|
||||
role: 'assistant',
|
||||
kind: 'reply',
|
||||
text: '设计任务已确认并开始执行。',
|
||||
quickReplies: [],
|
||||
generationQuote: null,
|
||||
turnRevision: 2,
|
||||
createdAt: '2026-07-31T10:01:00Z',
|
||||
}],
|
||||
};
|
||||
confirmImageWorkspaceGenerationMock.mockResolvedValueOnce(confirmedConversation);
|
||||
fetchImageWorkspaceTasksMock
|
||||
.mockResolvedValueOnce([taskFixture])
|
||||
.mockResolvedValueOnce([{
|
||||
...taskFixture,
|
||||
taskId: 'task-confirmed-visible',
|
||||
quoteId: 'quote-one',
|
||||
status: 'queued',
|
||||
resultAssets: [],
|
||||
}]);
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
await screen.findByTestId('design-quote-quote-one');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认并开始生成' }));
|
||||
|
||||
expect(await screen.findByText('确认生成')).toBeInTheDocument();
|
||||
expect(screen.getByText('设计任务已确认并开始执行。')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('converts an exact composer confirmation into the structured Quote action', async () => {
|
||||
fetchImageWorkspaceTasksMock
|
||||
.mockResolvedValueOnce([taskFixture])
|
||||
@@ -706,6 +1057,13 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
1,
|
||||
'quote-one',
|
||||
expect.stringMatching(/^turn-/),
|
||||
'夜色中的机械城堡角色海报,电影感构图。',
|
||||
{
|
||||
model: 'image-model-one',
|
||||
resolution: '1024x1024',
|
||||
aspectRatio: '1:1',
|
||||
durationSeconds: null,
|
||||
},
|
||||
));
|
||||
expect(sendImageWorkspaceMessageMock).not.toHaveBeenCalled();
|
||||
expect(await screen.findByTestId('design-task-task-confirmed-from-composer'))
|
||||
@@ -1012,6 +1370,9 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
||||
|
||||
render(<MemoryRouter><ImageCanvas /></MemoryRouter>);
|
||||
|
||||
const taskCard = await screen.findByTestId('design-task-task-one');
|
||||
expect(within(taskCard).queryByRole('alert')).not.toBeInTheDocument();
|
||||
fireEvent.click(within(taskCard).getByRole('button', { name: '查看生图任务详情' }));
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(alert).toHaveTextContent('内容可能涉及版权或安全风险,请调整设计后重试');
|
||||
expect(alert).not.toHaveTextContent('policy_blocked');
|
||||
|
||||
Reference in New Issue
Block a user