import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { YouthCreationCard } from '@/pages/ImageCanvas/YouthCreationCard'; import { DesignTaskPanel } from '@/pages/ImageCanvas/DesignProductionPanel'; import { useImageWorkspaceStore } from '@/stores/image-workspace'; import type { DesignCompilationIssue, DesignGenerationTask, } from '../../shared/image-workspace'; import { designQuoteFixture, designValuesFixture, designFormFixture, designWorkspaceFixture, } from '../fixtures/design-workspace-v2'; function renderCard( workspace = designWorkspaceFixture({ form: designFormFixture() }), options: { generationAvailable?: boolean; quoteBlockers?: DesignCompilationIssue[]; onOpenFineTune?: () => void; onFocusComposer?: () => void; onQuoteOffered?: () => void; } = {}, ) { const requestQuote = vi.fn().mockResolvedValue(workspace); const resolveDecisionPrompt = vi.fn().mockResolvedValue(workspace); const onOpenFineTune = options.onOpenFineTune ?? vi.fn(); const onFocusComposer = options.onFocusComposer ?? vi.fn(); const onQuoteOffered = options.onQuoteOffered ?? vi.fn(); useImageWorkspaceStore.setState({ workspace, quoteBlockers: [], pendingOperations: {}, requestQuote, resolveDecisionPrompt, }); render( , ); return { workspace, requestQuote, resolveDecisionPrompt, onOpenFineTune, onFocusComposer, onQuoteOffered, }; } function taskFixture( overrides: Partial = {}, ): DesignGenerationTask { const quote = designQuoteFixture(); return { taskId: 'task-1', quoteId: quote.quoteId, status: 'running', taskRevision: 1, specificationRevision: quote.specificationRevision, specificationRevisionId: quote.specificationRevisionId, specificationDigest: quote.specificationDigest, medium: quote.medium, compilerVersion: quote.compilerVersion, outputSummary: quote.outputSummary, maximumCustomerChargeAtoms: quote.maximumCustomerChargeAtoms, customerBilling: { quotedAtoms: 1200, heldAtoms: 1200, chargedAtoms: 0, refundedAtoms: 0, pendingResolutionAtoms: 0, }, customerHoldExpiresAt: quote.expiresAt, resolutionDeadlineAt: null, progress: { stage: 'generating', completedRequiredSteps: 0, totalRequiredSteps: 1, }, executionHealth: 'normal', cancellation: { state: 'available', outcome: null }, issues: [], resultAssetIds: [], nextActions: ['cancel'], createdAt: '2026-09-02T09:00:00.000Z', ...overrides, }; } describe('YouthCreationCard', () => { beforeEach(() => { vi.clearAllMocks(); }); it('shows a plain creation summary and reveals the offered quote after requesting it', async () => { const { onQuoteOffered, requestQuote } = renderCard(); expect(screen.getByRole('heading', { name: 'AI 听懂的想法' })).toBeInTheDocument(); expect(screen.getByText('会跟着聊天自动更新,不需要逐项填写。')).toBeInTheDocument(); expect(screen.getByText('把便携咖啡机呈现为城市通勤中的精密工具')).toBeInTheDocument(); expect(screen.getByText('图片')).toBeInTheDocument(); expect(screen.getByText('竖版海报')).toBeInTheDocument(); expect(screen.getByText('3:4')).toBeInTheDocument(); expect(within(screen.getByTestId('youth-creation-readiness')).getByText('想法已经清楚,可以检查制作方案')).toBeInTheDocument(); expect(screen.queryByText(/Specification|revision|compiler|provider|immutable/i)).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '看看制作方案' })); expect(requestQuote).toHaveBeenCalledOnce(); await waitFor(() => expect(onQuoteOffered).toHaveBeenCalledOnce()); }); it('does not surface legacy decision prompts as a form', () => { const workspace = designWorkspaceFixture({ form: designFormFixture({ decisionPrompts: [{ id: 'choice-1', kind: 'question', basedOnSpecificationRevisionId: 'specification-revision-3', targetPaths: ['visual.art_direction'], title: '画面想要什么感觉?', body: '选一个你喜欢的方向。', options: [ { id: 'cartoon', label: '有趣的卡通', description: '颜色明快,轻松有趣' }, { id: 'cinema', label: '像电影一样', description: null }, { id: 'delegate', label: '你帮我选', description: null }, ], }, { id: 'choice-2', kind: 'question', basedOnSpecificationRevisionId: 'specification-revision-3', targetPaths: ['visual.palette'], title: '这个问题不该显示', body: '只显示一个选择就好。', options: [{ id: 'blue', label: '蓝色', description: null }], }], }), }); const { resolveDecisionPrompt } = renderCard(workspace); expect(screen.queryByTestId('youth-creation-choice')).not.toBeInTheDocument(); expect(screen.queryByText('画面想要什么感觉?')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /有趣的卡通/ })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /你帮我选/ })).not.toBeInTheDocument(); expect(screen.queryByText('这个问题不该显示')).not.toBeInTheDocument(); expect(resolveDecisionPrompt).not.toHaveBeenCalled(); }); it('shows known must-have, must-avoid, reference, video, and plain blocker details only', () => { const values = structuredClone(designValuesFixture); values.intent.media = 'video'; values.video.total_duration_seconds = 10; values.video.pacing = '很有活力'; values.video.end_state = '机器人端出一盘星星饼干'; values.video.shots = [{ id: 'shot-1', start_milliseconds: 0, end_milliseconds: 10_000, story_beat: '机器人在厨房里做饭', framing: 'close-up', camera: 'dolly-in', subject_motion: '快速搅拌', environment_motion: null, focus_behavior: null, transition: null, }]; values.constraints.must_include = ['远处的地球']; values.constraints.must_preserve = ['红围巾']; values.constraints.must_avoid = ['真实人物']; values.references = [{ id: 'reference-1', asset_id: 'asset-reference', asset_revision: null, role: 'subject_identity', preserve: ['圆脸'], adapt: ['背景颜色'], do_not_copy: ['原图文字'], reviewed_observations: ['白色背景'], }]; const workspace = designWorkspaceFixture({ form: designFormFixture({ specification: { schema_version: 1, values, field_decisions: {} }, }), assets: [{ assetId: 'asset-reference', workspaceId: 'workspace-1', role: 'uploaded', mediaType: 'image', mimeType: 'image/png', width: 600, height: 600, durationMilliseconds: null, generationTaskId: null, createdAt: '2026-09-02T09:00:00.000Z', contentPath: '/api/design/assets/asset-reference/content', }], }); const blocker: DesignCompilationIssue = { code: 'aspect_ratio_required', message: 'Choose an aspect ratio supported by the frozen route.', severity: 'blocker', path: 'output.aspect_ratio', }; renderCard(workspace, { quoteBlockers: [blocker] }); fireEvent.click(screen.getByText('查看画面细节')); expect(screen.getByText('一定要有')).toBeInTheDocument(); expect(screen.getByText('远处的地球')).toBeInTheDocument(); expect(screen.getByText('想保留')).toBeInTheDocument(); expect(screen.getByText('红围巾')).toBeInTheDocument(); expect(screen.getByText('不要出现')).toBeInTheDocument(); expect(screen.getByText('真实人物')).toBeInTheDocument(); expect(screen.getByText('参考图片')).toBeInTheDocument(); expect(screen.getByText('600 × 600')).toBeInTheDocument(); expect(screen.getByText('圆脸')).toBeInTheDocument(); expect(screen.getByText('背景颜色')).toBeInTheDocument(); expect(screen.getByText('原图文字')).toBeInTheDocument(); expect(screen.getByText('视频安排')).toBeInTheDocument(); expect(screen.getByText('10 秒')).toBeInTheDocument(); expect(screen.getByText('很有活力')).toBeInTheDocument(); expect(screen.getByText('机器人端出一盘星星饼干')).toBeInTheDocument(); expect(screen.getByText(/机器人在厨房里做饭/)).toBeInTheDocument(); expect(screen.getByText('先选择作品形状。')).toBeInTheDocument(); expect(screen.queryByText(/frozen route/i)).not.toBeInTheDocument(); expect(screen.queryByText('dolly-in')).not.toBeInTheDocument(); expect(screen.queryByText('milliseconds')).not.toBeInTheDocument(); expect(screen.queryByText(/Specification|revision|compiler|provider|immutable/i)).not.toBeInTheDocument(); }); it('uses a gentle empty-state CTA and sends focus back to the composer', () => { const values = structuredClone(designValuesFixture); values.intent.media = null; values.intent.creation_intent = null; values.intent.purpose = null; values.intent.channel = null; values.intent.audience = null; values.content.concept = null; values.output.aspect_ratio = null; values.output.orientation = null; const onFocusComposer = vi.fn(); const { onOpenFineTune } = renderCard( designWorkspaceFixture({ form: designFormFixture({ specification: { schema_version: 1, values, field_decisions: {} }, }), turns: [], }), { onFocusComposer }, ); expect(within(screen.getByTestId('youth-creation-readiness')).getByText('等你的第一个想法')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '开始描述' })); expect(onFocusComposer).toHaveBeenCalledOnce(); fireEvent.click(screen.getByRole('button', { name: '手动调整(可选)' })); expect(onOpenFineTune).toHaveBeenCalledOnce(); }); it('does not request a plan when generation is unavailable', () => { const { requestQuote, onFocusComposer } = renderCard(undefined, { generationAvailable: false, }); expect(screen.getByRole('button', { name: '继续完善想法' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '继续完善想法' })); expect(requestQuote).not.toHaveBeenCalled(); expect(onFocusComposer).toHaveBeenCalledOnce(); }); it('keeps confirming and generating states clear, then offers another version after a result', () => { const confirming = designWorkspaceFixture({ form: designFormFixture({ activeQuotes: [designQuoteFixture()] }), }); const { onOpenFineTune: onConfirmingFineTune } = renderCard(confirming); expect(screen.getByText('方案准备好了,确认后就开始制作。')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '继续调整' })); expect(onConfirmingFineTune).toHaveBeenCalledOnce(); const resultAsset = { assetId: 'asset-result', workspaceId: 'workspace-1', role: 'generated' as const, mediaType: 'image' as const, mimeType: 'image/png', width: 1024, height: 1365, durationMilliseconds: null, generationTaskId: 'task-1', createdAt: '2026-09-02T09:05:00.000Z', contentPath: '/api/design/assets/asset-result/content', }; const generating = designWorkspaceFixture({ tasks: [taskFixture()] }); const result = designWorkspaceFixture({ tasks: [taskFixture({ status: 'succeeded', progress: { stage: 'completed', completedRequiredSteps: 1, totalRequiredSteps: 1 }, resultAssetIds: ['asset-result'], nextActions: ['view_result', 'revise_design', 'generate_again'], })], assets: [resultAsset], }); // A card is intentionally remounted between resource states so each assertion // observes the state a user sees at that moment. cleanup(); renderCard(generating); expect(within(screen.getByTestId('youth-creation-readiness')).getByText('正在制作,请稍等一下')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '正在制作' })).toBeDisabled(); cleanup(); const { requestQuote, onOpenFineTune } = renderCard(result); expect(within(screen.getByTestId('youth-creation-readiness')).getByText('完成了,看看你的作品吧')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '继续调整' })); expect(onOpenFineTune).toHaveBeenCalledOnce(); fireEvent.click(screen.getByRole('button', { name: '做一个新版本' })); expect(requestQuote).toHaveBeenCalledOnce(); }); it('shows safe Chinese task guidance instead of provider messages', () => { render(); expect(screen.getByText('作品没有通过内容检查,可以换一种安全的说法再试。')).toBeInTheDocument(); expect(screen.queryByText(/Provider policy/i)).not.toBeInTheDocument(); }); });