import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { DesignPlanHistory } from '@/pages/ImageCanvas/DesignPlanHistory'; import { YouthCreationCard } from '@/pages/ImageCanvas/YouthCreationCard'; import { useImageWorkspaceStore } from '@/stores/image-workspace'; import type { DesignGenerationTask } from '../../shared/image-workspace'; import { designFormFixture, designQuoteFixture, designValuesFixture, designWorkspaceFixture, } from '../fixtures/design-workspace-v2'; const { uploadMock } = vi.hoisted(() => ({ uploadMock: vi.fn() })); vi.mock('@/lib/image-workspace', async (importOriginal) => { const original = await importOriginal(); return { ...original, resolveImageWorkspaceAssetUrl: vi.fn().mockReturnValue(new Promise(() => undefined)), saveImageWorkspaceAsset: vi.fn().mockResolvedValue({ status: 'saved' }), uploadImageWorkspaceAsset: uploadMock, }; }); 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, }; } function renderCard(workspace = designWorkspaceFixture()) { const actions = { applyFieldOperations: vi.fn().mockResolvedValue(workspace), requestQuote: vi.fn().mockResolvedValue(workspace), confirmGeneration: vi.fn().mockResolvedValue(workspace), }; useImageWorkspaceStore.setState({ workspace, quoteBlockers: [], pendingOperations: {}, ...actions, }); render( , ); return actions; } function referenceWorkspace({ offered = false } = {}) { const values = structuredClone(designValuesFixture); values.content.concept = '参考 @图片1 的小狗,让它在草地上奔跑'; values.references = [{ id: 'reference-1', asset_id: 'asset-reference', asset_revision: null, role: 'subject_identity', preserve: ['圆脸'], adapt: ['背景颜色'], do_not_copy: ['原图文字'], reviewed_observations: ['白色背景'], }]; return designWorkspaceFixture({ form: designFormFixture({ specification: { schema_version: 1, values, field_decisions: {} }, activeQuotes: offered ? [designQuoteFixture()] : [], }), 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', }], }); } describe('YouthCreationCard', () => { beforeEach(() => { vi.clearAllMocks(); useImageWorkspaceStore.getState().reset(); }); it('puts the editable creator prompt and compact visible parameters in one plan card', () => { renderCard(); expect(screen.getByRole('heading', { name: '制作方案' })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: '创作提示词' })).toHaveValue( '把便携咖啡机呈现为城市通勤中的精密工具', ); expect(screen.getByRole('combobox', { name: '类型' })).toHaveValue('image'); expect(screen.getByRole('combobox', { name: '画幅' })).toHaveValue('3:4'); expect(screen.getByRole('button', { name: '增加数量' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '准备制作方案' })).toBeInTheDocument(); expect(screen.queryByText('查看画面细节')).not.toBeInTheDocument(); expect(screen.queryByText('手动调整')).not.toBeInTheDocument(); }); it('saves a final-prompt edit through the public Specification field', async () => { const actions = renderCard(); const prompt = screen.getByRole('textbox', { name: '创作提示词' }); fireEvent.change(prompt, { target: { value: '更聚焦咖啡机,背景使用清晨地铁站' } }); fireEvent.blur(prompt); await waitFor(() => { expect(actions.applyFieldOperations).toHaveBeenCalledWith([ { kind: 'set', path: 'content.concept', value: '更聚焦咖啡机,背景使用清晨地铁站' }, ]); }); }); it('recompiles and requotes after editing an offered plan', async () => { const workspace = designWorkspaceFixture({ form: designFormFixture({ activeQuotes: [designQuoteFixture()] }), }); const actions = renderCard(workspace); const prompt = screen.getByRole('textbox', { name: '创作提示词' }); fireEvent.change(prompt, { target: { value: '改成夜晚城市街景' } }); fireEvent.blur(prompt); await waitFor(() => expect(actions.applyFieldOperations).toHaveBeenCalledOnce()); await waitFor(() => expect(actions.requestQuote).toHaveBeenCalledOnce()); }); it('keeps reference rows binding-only while the prompt carries image usage', () => { renderCard(referenceWorkspace()); expect(screen.getByRole('textbox', { name: '创作提示词' })).toHaveValue( '参考 @图片1 的小狗,让它在草地上奔跑', ); expect(screen.getByText('@图片1')).toBeInTheDocument(); expect(screen.getByText('已引用')).toBeInTheDocument(); expect(screen.getByText('参考图 1 · 600×600')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '替换参考图 1' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '删除参考图 1' })).toBeInTheDocument(); expect(screen.queryByText('圆脸')).not.toBeInTheDocument(); expect(screen.queryByText('背景颜色')).not.toBeInTheDocument(); expect(screen.queryByText('原图文字')).not.toBeInTheDocument(); expect(screen.queryByText(/主体参考|最想保留|可以改变|不要照着画/)).not.toBeInTheDocument(); }); it('offers existing reference tokens when the user types @', () => { renderCard(referenceWorkspace()); const prompt = screen.getByRole('textbox', { name: '创作提示词' }); const nextValue = '让 '; fireEvent.change(prompt, { target: { value: `${nextValue}@`, selectionStart: nextValue.length + 1 }, }); expect(screen.getByRole('listbox', { name: '可引用的图片' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: '@图片1' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: '上传新的参考图' })).toBeInTheDocument(); }); it('does not duplicate a reference token that is already present', () => { const actions = renderCard(referenceWorkspace()); fireEvent.click(screen.getByTitle('在提示词中插入 @图片1')); expect(screen.getByRole('textbox', { name: '创作提示词' })).toHaveValue( '参考 @图片1 的小狗,让它在草地上奔跑', ); expect(actions.applyFieldOperations).not.toHaveBeenCalled(); }); it('blocks confirmation while a prompt alias has no bound image', () => { const workspace = referenceWorkspace({ offered: true }); workspace.form.specification.values.content.concept = '参考 @图片1 和 @图片2 制作双人合照'; renderCard(workspace); expect(screen.getByText('@图片2 还没有绑定图片')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '上传 @图片2' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '确认并开始制作' })).toBeDisabled(); }); it('binds an already typed next alias without appending it again', async () => { const workspace = referenceWorkspace(); workspace.form.specification.values.content.concept = '参考 @图片1 和 @图片2 制作双人合照'; uploadMock.mockResolvedValueOnce({ assetId: 'asset-reference-2', workspaceId: 'workspace-1', role: 'uploaded', mediaType: 'image', mimeType: 'image/png', width: 800, height: 800, durationMilliseconds: null, generationTaskId: null, createdAt: '2026-09-02T10:00:00.000Z', contentPath: '/api/design/assets/asset-reference-2/content', }); const actions = renderCard(workspace); fireEvent.change(screen.getByLabelText('选择参考图片'), { target: { files: [new File(['image'], 'second.png', { type: 'image/png' })] }, }); await waitFor(() => expect(actions.applyFieldOperations).toHaveBeenCalledOnce()); const operations = actions.applyFieldOperations.mock.calls[0]?.[0]; expect(operations).toHaveLength(1); expect(operations?.[0]).toMatchObject({ kind: 'set', path: 'references', value: [ expect.objectContaining({ asset_id: 'asset-reference' }), expect.objectContaining({ asset_id: 'asset-reference-2' }), ], }); expect(screen.getByRole('textbox', { name: '创作提示词' })).toHaveValue( '参考 @图片1 和 @图片2 制作双人合照', ); }); it('removes a binding and its prompt token atomically', async () => { const actions = renderCard(referenceWorkspace()); fireEvent.click(screen.getByRole('button', { name: '删除参考图 1' })); await waitFor(() => { expect(actions.applyFieldOperations).toHaveBeenCalledWith([ { kind: 'clear', path: 'references' }, { kind: 'set', path: 'content.concept', value: '参考 的小狗,让它在草地上奔跑' }, ]); }); }); it('submits compact parameter edits and confirms only the offered Quote identity', async () => { const workspace = referenceWorkspace({ offered: true }); const actions = renderCard(workspace); fireEvent.change(screen.getByRole('combobox', { name: '画幅' }), { target: { value: '16:9' } }); await waitFor(() => { expect(actions.applyFieldOperations).toHaveBeenCalledWith([ { kind: 'set', path: 'output.aspect_ratio', value: '16:9' }, { kind: 'set', path: 'output.orientation', value: 'landscape' }, ]); }); await waitFor(() => expect(actions.requestQuote).toHaveBeenCalledOnce()); fireEvent.click(screen.getByRole('button', { name: '确认并开始制作' })); expect(actions.confirmGeneration).toHaveBeenCalledWith('quote-1'); }); it('collapses a confirmed plan into the expandable production history', () => { const workspace = designWorkspaceFixture({ tasks: [taskFixture()] }); useImageWorkspaceStore.setState({ pendingOperations: {} }); const { container } = render( <> , ); expect(screen.getByTestId('design-plan-history-task-1')).toBeInTheDocument(); expect(screen.getByText('制作中')).toBeInTheDocument(); expect(container.querySelector('[data-testid="youth-creation-card"]')).toBeNull(); }); it('shows safe Chinese task guidance instead of provider messages', () => { const workspace = designWorkspaceFixture({ tasks: [taskFixture({ status: 'failed', progress: { stage: 'failed', completedRequiredSteps: 0, totalRequiredSteps: 1 }, issues: [{ code: 'asset_rejected', message: 'Provider policy rejected the generated asset.', recoveryOwner: 'platform', }], nextActions: ['revise_design'], })], }); render(); fireEvent.click(screen.getByText('制作记录')); expect(screen.getByText('作品没有通过内容检查,可以换一种安全的说法再试。')).toBeInTheDocument(); expect(screen.queryByText(/Provider policy/i)).not.toBeInTheDocument(); }); });