// @vitest-environment jsdom import { act, fireEvent, render, screen, waitFor, within, } from '@testing-library/react'; import { afterEach, describe, expect, test, vi } from 'vitest'; import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import * as puzzleWorksService from '../../services/puzzle-works'; import { puzzleAssetClient } from '../../services/puzzle-works/puzzleAssetClient'; import { PuzzleResultView } from './PuzzleResultView'; vi.mock('../ResolvedAssetImage', () => ({ ResolvedAssetImage: ({ src, alt, className, 'data-testid': dataTestId, }: { src?: string | null; alt?: string; className?: string; 'data-testid'?: string; }) => ( src ? ( {alt} ) : null ), })); vi.mock('../../services/puzzle-works/puzzleAssetClient', () => ({ puzzleAssetClient: { listHistoryAssets: vi.fn(), }, })); vi.mock('../../services/puzzle-works', () => ({ updatePuzzleWork: vi.fn(), })); vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({ useResolvedAssetReadUrl: (src?: string | null) => ({ resolvedUrl: src ? `https://signed.example.com/${src.replace(/^\/+/u, '')}` : '', isResolving: false, shouldResolve: Boolean(src?.trim().startsWith('/generated-')), }), })); afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); }); function createSession( overrides: Partial = {}, ): PuzzleAgentSessionSnapshot { const anchorPack = { themePromise: { key: 'themePromise', label: '题材承诺', value: '雨夜猫咪', status: 'confirmed' as const, }, visualSubject: { key: 'visualSubject', label: '画面主体', value: '屋檐下的猫', status: 'confirmed' as const, }, visualMood: { key: 'visualMood', label: '视觉气质', value: '温暖', status: 'confirmed' as const, }, compositionHooks: { key: 'compositionHooks', label: '拼图记忆点', value: '雨滴与灯牌', status: 'confirmed' as const, }, tagsAndForbidden: { key: 'tagsAndForbidden', label: '标签与禁忌', value: '猫咪、雨夜', status: 'confirmed' as const, }, }; const level = { levelId: 'puzzle-level-1', levelName: '雨夜猫街', pictureDescription: '屋檐下的猫与暖灯街角。', candidates: [ { candidateId: 'candidate-1', imageSrc: '/puzzle/candidate-1.png', assetId: 'asset-1', prompt: '雨夜猫咪', actualPrompt: null, sourceType: 'generated' as const, selected: true, }, ], selectedCandidateId: 'candidate-1', coverImageSrc: '/puzzle/candidate-1.png', coverAssetId: 'asset-1', generationStatus: 'ready' as const, }; const baseSession: PuzzleAgentSessionSnapshot = { sessionId: 'puzzle-session-1', currentTurn: 2, progressPercent: 88, stage: 'ready_to_publish', anchorPack, draft: { workTitle: overrides.draft?.workTitle ?? '暖灯猫街作品', workDescription: overrides.draft?.workDescription ?? '一套雨夜猫街主题拼图。', levelName: level.levelName, summary: level.pictureDescription, themeTags: overrides.draft?.themeTags ?? ['猫咪', '雨夜', '暖灯'], forbiddenDirectives: [], creatorIntent: null, anchorPack, candidates: level.candidates, selectedCandidateId: level.selectedCandidateId, coverImageSrc: level.coverImageSrc, coverAssetId: level.coverAssetId, generationStatus: 'ready', levels: [level], metadata: null, ...overrides.draft, }, messages: [], lastAssistantReply: null, publishedProfileId: null, suggestedActions: [], resultPreview: null, updatedAt: '2026-04-26T10:00:00.000Z', }; const session = { ...baseSession, resultPreview: { draft: baseSession.draft!, publishReady: true, blockers: [], qualityFindings: [], }, ...overrides, } satisfies PuzzleAgentSessionSnapshot; return session; } describe('PuzzleResultView', () => { test('renders level list and work info tabs', () => { render( {}} onExecuteAction={() => {}} onStartTestRun={() => {}} />, ); expect(screen.getByRole('button', { name: '拼图关卡' })).toBeTruthy(); expect(screen.getByRole('button', { name: '作品信息' })).toBeTruthy(); expect(screen.getByRole('button', { name: '素材配置' })).toBeTruthy(); expect(screen.queryByRole('button', { name: '音乐' })).toBeNull(); expect(screen.getByText('雨夜猫街')).toBeTruthy(); expect(screen.getByText('获得更多积分激励')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: '作品信息' })); expect(screen.getByLabelText('作品名称')).toHaveProperty( 'value', '暖灯猫街作品', ); expect(screen.getByLabelText('作品描述')).toHaveProperty( 'value', '一套雨夜猫街主题拼图。', ); }); test('result action bar restores draft trial entry', () => { const onStartTestRun = vi.fn(); render( {}} onExecuteAction={() => {}} onStartTestRun={onStartTestRun} />, ); fireEvent.click(screen.getByRole('button', { name: '试玩' })); expect(onStartTestRun).toHaveBeenCalledWith( expect.objectContaining({ workTitle: '暖灯猫街作品', levels: [ expect.objectContaining({ levelId: 'puzzle-level-1', levelName: '雨夜猫街', }), ], }), ); }); test('auto saves work info and levels through one payload', async () => { vi.useFakeTimers(); vi.mocked(puzzleWorksService.updatePuzzleWork).mockResolvedValue({ item: {} as never, }); render( {}} onExecuteAction={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '作品信息' })); fireEvent.change(screen.getByLabelText('作品名称'), { target: { value: '暖灯猫街合集' }, }); await act(async () => { await vi.runAllTimersAsync(); }); expect(puzzleWorksService.updatePuzzleWork).toHaveBeenCalledWith( 'puzzle-profile-session-1', expect.objectContaining({ workTitle: '暖灯猫街合集', workDescription: '一套雨夜猫街主题拼图。', levelName: '雨夜猫街', summary: '一套雨夜猫街主题拼图。', themeTags: ['猫咪', '雨夜', '暖灯'], levels: expect.arrayContaining([ expect.objectContaining({ levelId: 'puzzle-level-1', levelName: '雨夜猫街', }), ]), }), ); }); test('opens an independent level detail dialog for generation and test play', () => { const onExecuteAction = vi.fn(); const onStartTestRun = vi.fn(); render( {}} onExecuteAction={onExecuteAction} onStartTestRun={onStartTestRun} />, ); fireEvent.click(screen.getByText('雨夜猫街')); const dialog = screen.getByRole('dialog', { name: '关卡详情' }); fireEvent.change(within(dialog).getByLabelText('关卡名称'), { target: { value: '暖灯猫街' }, }); fireEvent.change(within(dialog).getByLabelText('画面描述'), { target: { value: '一只猫在雨夜灯牌下回头。' }, }); fireEvent.click( within(dialog).getByRole('button', { name: /重新生成画面/u }), ); const confirmDialog = screen.getByRole('dialog', { name: '确认消耗泥点', }); expect(within(confirmDialog).getByText('消耗 2 泥点')).toBeTruthy(); fireEvent.click(within(confirmDialog).getByRole('button', { name: '确定' })); expect(onExecuteAction).toHaveBeenCalledWith({ action: 'generate_puzzle_images', levelId: 'puzzle-level-1', promptText: '一只猫在雨夜灯牌下回头。', referenceImageSrc: undefined, imageModel: 'gpt-image-2', aiRedraw: true, candidateCount: 1, workTitle: '暖灯猫街作品', workDescription: '一套雨夜猫街主题拼图。', summary: '一套雨夜猫街主题拼图。', themeTags: ['猫咪', '雨夜', '暖灯'], levelsJson: expect.any(String), }); expect(screen.getByRole('progressbar', { name: '画面生成进度' })).toBeTruthy(); const generatePayload = onExecuteAction.mock.calls[0]![0]; expect(JSON.parse(generatePayload.levelsJson ?? '[]')).toEqual([ expect.objectContaining({ levelId: 'puzzle-level-1', levelName: '暖灯猫街', pictureDescription: '一只猫在雨夜灯牌下回头。', generationStatus: 'generating', }), ]); expect(within(dialog).getByText('预计剩余 90 秒')).toBeTruthy(); expect( within(dialog).queryByPlaceholderText('参考图链接或资产ID'), ).toBeNull(); const levelList = screen.getByLabelText('拼图关卡列表'); expect(within(levelList).getAllByText('生成中').length).toBeGreaterThan(0); const levelNameInput = within(dialog).getByLabelText('关卡名称'); const formalImageTitle = within(dialog).getByText('画面图'); const pictureDescriptionInput = within(dialog).getByLabelText('画面描述'); expect( levelNameInput.compareDocumentPosition(formalImageTitle) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); expect( formalImageTitle.compareDocumentPosition(pictureDescriptionInput) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); fireEvent.click(within(dialog).getByRole('button', { name: /关卡测试/u })); expect(onStartTestRun).toHaveBeenCalledWith( expect.objectContaining({ levelName: '暖灯猫街', summary: '一套雨夜猫街主题拼图。', levels: [ expect.objectContaining({ levelId: 'puzzle-level-1', levelName: '暖灯猫街', }), ], }), ); }); test('adds and deletes levels from the list', async () => { vi.useFakeTimers(); vi.mocked(puzzleWorksService.updatePuzzleWork).mockResolvedValue({ item: {} as never, }); render( {}} onExecuteAction={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: /新增关卡/u })); const dialog = screen.getByRole('dialog', { name: '关卡详情' }); expect( within(dialog).getByRole('button', { name: /生成画面/u }), ).toBeTruthy(); expect(within(dialog).getByText('消耗2泥点')).toBeTruthy(); expect( within(dialog).getByText('等待时间可以制作更多关卡哦~'), ).toBeTruthy(); expect(within(dialog).getByText('画面图')).toBeTruthy(); expect( within(dialog).queryByRole('button', { name: /关卡测试/u }), ).toBeNull(); fireEvent.click(screen.getByLabelText('关闭')); expect(screen.getAllByText('第2关').length).toBeGreaterThan(0); await act(async () => { await vi.runAllTimersAsync(); }); expect(puzzleWorksService.updatePuzzleWork).toHaveBeenLastCalledWith( 'puzzle-profile-session-1', expect.objectContaining({ levels: expect.arrayContaining([ expect.objectContaining({ levelId: 'puzzle-level-1' }), expect.objectContaining({ levelName: '' }), ]), }), ); fireEvent.click(screen.getByLabelText('删除关卡 第2关')); expect(screen.queryByText('第2关')).toBeNull(); await act(async () => { await vi.runAllTimersAsync(); }); expect(puzzleWorksService.updatePuzzleWork).toHaveBeenLastCalledWith( 'puzzle-profile-session-1', expect.objectContaining({ levels: [ expect.objectContaining({ levelId: 'puzzle-level-1', }), ], }), ); }); test('generates image for a newly added level with the current levels snapshot', () => { vi.spyOn(Date, 'now').mockReturnValue(1_775_000_000_000); const onExecuteAction = vi.fn(); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByRole('button', { name: /新增关卡/u })); const dialog = screen.getByRole('dialog', { name: '关卡详情' }); fireEvent.change(within(dialog).getByLabelText('画面描述'), { target: { value: '新关卡里有一座发光钟楼。' }, }); fireEvent.click(within(dialog).getByRole('button', { name: /生成画面/u })); fireEvent.click( within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole( 'button', { name: '确定' }, ), ); expect(onExecuteAction).toHaveBeenCalledWith({ action: 'generate_puzzle_images', levelId: 'puzzle-level-1775000000000-2', promptText: '新关卡里有一座发光钟楼。', referenceImageSrc: undefined, imageModel: 'gpt-image-2', aiRedraw: true, candidateCount: 1, workTitle: '暖灯猫街作品', workDescription: '一套雨夜猫街主题拼图。', summary: '一套雨夜猫街主题拼图。', themeTags: ['猫咪', '雨夜', '暖灯'], levelsJson: expect.any(String), }); const payload = onExecuteAction.mock.calls[0]![0]; expect(JSON.parse(payload.levelsJson ?? '[]')).toEqual([ expect.objectContaining({ levelId: 'puzzle-level-1' }), expect.objectContaining({ levelId: 'puzzle-level-1775000000000-2', levelName: '', pictureDescription: '新关卡里有一座发光钟楼。', generationStatus: 'generating', }), ]); }); test('keeps generation progress visible after closing and reopening level dialog', () => { const onExecuteAction = vi.fn(); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByText('雨夜猫街')); fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u })); fireEvent.click( within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole( 'button', { name: '确定' }, ), ); fireEvent.click(screen.getByLabelText('关闭')); expect( within(screen.getByLabelText('拼图关卡列表')).getAllByText('生成中') .length, ).toBeGreaterThan(0); fireEvent.click(screen.getByText('雨夜猫街')); const reopenedDialog = screen.getByRole('dialog', { name: '关卡详情' }); expect( within(reopenedDialog).getByRole('progressbar', { name: '画面生成进度' }), ).toBeTruthy(); expect(within(reopenedDialog).getByText('预计剩余 90 秒')).toBeTruthy(); }); test('allows parallel draft editing while a level image is generating but blocks publish', () => { const onExecuteAction = vi.fn(); const onStartTestRun = vi.fn(); render( {}} onExecuteAction={onExecuteAction} onStartTestRun={onStartTestRun} />, ); fireEvent.click(screen.getByText('雨夜猫街')); fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u })); fireEvent.click( within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole( 'button', { name: '确定' }, ), ); fireEvent.change(screen.getByLabelText('关卡名称'), { target: { value: '继续编辑的猫街' }, }); fireEvent.click(screen.getByRole('button', { name: /关卡测试/u })); expect(onStartTestRun).toHaveBeenCalledWith( expect.objectContaining({ levelName: '继续编辑的猫街', }), ); fireEvent.click(screen.getByLabelText('关闭')); fireEvent.click(screen.getByRole('button', { name: /新增关卡/u })); expect(screen.getByRole('dialog', { name: '关卡详情' })).toBeTruthy(); fireEvent.click(screen.getByLabelText('关闭')); fireEvent.click(screen.getByRole('button', { name: /发布/u })); const publishDialog = screen.getByRole('dialog', { name: '发布拼图作品' }); expect(within(publishDialog).getByText('还有关卡画面正在生成。')).toBeTruthy(); expect( within(publishDialog).getByRole('button', { name: /发布到广场/u }), ).toHaveProperty('disabled', true); }); test('publishes with work info and serialized levels', () => { const onExecuteAction = vi.fn(); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByRole('button', { name: /发布/u })); fireEvent.click( within(screen.getByRole('dialog', { name: '发布拼图作品' })).getByRole( 'button', { name: /发布到广场/u }, ), ); expect(onExecuteAction).toHaveBeenCalledWith( expect.objectContaining({ action: 'publish_puzzle_work', workTitle: '暖灯猫街作品', workDescription: '一套雨夜猫街主题拼图。', levelName: '雨夜猫街', summary: '一套雨夜猫街主题拼图。', themeTags: ['猫咪', '雨夜', '暖灯'], }), ); const payload = onExecuteAction.mock.calls[0]![0]; expect(JSON.parse(payload.levelsJson)).toEqual([ expect.objectContaining({ levelId: 'puzzle-level-1', levelName: '雨夜猫街', }), ]); }); test('keeps publish dialog open and shows backend publish error', () => { const onExecuteAction = vi.fn(); const { rerender } = render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByRole('button', { name: /发布/u })); const dialog = screen.getByRole('dialog', { name: '发布拼图作品' }); fireEvent.click( within(dialog).getByRole('button', { name: /发布到广场/u }), ); rerender( {}} onExecuteAction={onExecuteAction} />, ); const publishDialog = screen.getByRole('dialog', { name: '发布拼图作品', }); expect(publishDialog).toBeTruthy(); expect(within(publishDialog).getByText('泥点余额不足')).toBeTruthy(); }); test('generates six tags after work title and description are filled', () => { const onExecuteAction = vi.fn(); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByRole('button', { name: '作品信息' })); fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' })); expect(screen.getByText('请先填写作品名称和作品描述。')).toBeTruthy(); expect(onExecuteAction).not.toHaveBeenCalled(); fireEvent.change(screen.getByLabelText('作品描述'), { target: { value: '一套雨夜猫街主题拼图。' }, }); fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' })); expect(onExecuteAction).toHaveBeenCalledWith({ action: 'generate_puzzle_tags', workTitle: '雨夜猫街', workDescription: '一套雨夜猫街主题拼图。', levelName: '雨夜猫街', summary: '一套雨夜猫街主题拼图。', themeTags: [], levelsJson: expect.any(String), }); }); test('renders UI background tab with saved prompt and runtime preview', () => { const base = createSession(); const level = base.draft!.levels![0]!; render( {}} onExecuteAction={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '素材配置' })); expect(screen.getByAltText('拼图UI背景图').getAttribute('src')).toBe( '/generated-puzzle-assets/session/ui/background.png', ); expect(screen.getByLabelText('拼图UI背景提示词')).toHaveProperty( 'value', '雨夜猫街竖屏拼图UI背景', ); fireEvent.click(screen.getByRole('button', { name: '预览UI' })); const preview = screen.getByRole('dialog', { name: 'UI预览' }); expect( within(preview) .getByTestId('puzzle-ui-runtime-preview-background') .getAttribute('src'), ).toBe('/generated-puzzle-assets/session/ui/background.png'); expect(within(preview).getByLabelText('拼图区边界')).toBeTruthy(); }); test('UI背景只有 objectKey 时草稿页仍显示生成图', () => { const base = createSession(); const level = base.draft!.levels![0]!; render( {}} onExecuteAction={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '素材配置' })); expect(screen.getByAltText('拼图UI背景图').getAttribute('src')).toBe( '/generated-puzzle-assets/session/ui/background-object-key.png', ); expect(screen.getByRole('button', { name: /重新生成/u })).toBeTruthy(); }); test('does not display local fallback as saved UI background prompt', () => { render( {}} onExecuteAction={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '素材配置' })); expect(screen.getByLabelText('拼图UI背景提示词')).toHaveProperty( 'value', '', ); }); test('generates UI background with edited prompt and current levels snapshot', () => { const onExecuteAction = vi.fn(); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByRole('button', { name: '素材配置' })); fireEvent.change(screen.getByLabelText('拼图UI背景提示词'), { target: { value: '新拼图UI背景提示词' }, }); expect(screen.getByRole('button', { name: /生成UI背景 · 2泥点/u })).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: /生成UI背景/u })); const confirmDialog = screen.getByRole('dialog', { name: '确认消耗泥点', }); expect(within(confirmDialog).getByText('消耗 2 泥点')).toBeTruthy(); fireEvent.click(within(confirmDialog).getByRole('button', { name: '确定' })); expect(onExecuteAction).toHaveBeenCalledWith({ action: 'generate_puzzle_ui_background', levelId: 'puzzle-level-1', promptText: '新拼图UI背景提示词', workTitle: '暖灯猫街作品', workDescription: '一套雨夜猫街主题拼图。', summary: '一套雨夜猫街主题拼图。', themeTags: ['猫咪', '雨夜', '暖灯'], levelsJson: expect.any(String), }); const payload = onExecuteAction.mock.calls[0]![0]; expect(JSON.parse(payload.levelsJson ?? '[]')).toEqual([ expect.objectContaining({ levelId: 'puzzle-level-1', uiBackgroundPrompt: '新拼图UI背景提示词', }), ]); }); test('素材配置隐藏背景音乐入口', () => { const base = createSession(); const level = base.draft!.levels![0]!; render( {}} onExecuteAction={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '素材配置' })); expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull(); expect(screen.queryByRole('button', { name: /重新生成音乐/u })).toBeNull(); expect(screen.queryByLabelText('拼图背景音乐')).toBeNull(); }); test('生成完成回包合并历史音乐和UI背景后试玩使用最新资源', () => { const onStartTestRun = vi.fn(); const base = createSession(); const localLevel = { ...base.draft!.levels![0]!, generationStatus: 'generating' as const, uiBackgroundPrompt: '旧的UI背景提示词', uiBackgroundImageSrc: null, backgroundMusic: null, }; const incomingLevel = { ...localLevel, generationStatus: 'ready' as const, uiBackgroundPrompt: '水果乐园UI背景', uiBackgroundImageSrc: '/generated-puzzle-assets/session/ui/fruit-background.png', uiBackgroundImageObjectKey: 'generated-puzzle-assets/session/ui/fruit-background.png', backgroundMusic: { taskId: 'music-task-fruit', provider: 'vector-engine-suno', assetObjectId: 'asset-music-fruit', assetKind: 'puzzle_background_music', audioSrc: '/generated-puzzle-assets/session/audio/fruit.mp3', prompt: '', title: '水果乐园', updatedAt: '2026-05-14T10:00:00.000Z', }, }; const { rerender } = render( {}} onExecuteAction={() => {}} onStartTestRun={onStartTestRun} />, ); rerender( {}} onExecuteAction={() => {}} onStartTestRun={onStartTestRun} />, ); fireEvent.click(screen.getByRole('button', { name: '素材配置' })); expect(screen.getByAltText('拼图UI背景图').getAttribute('src')).toBe( '/generated-puzzle-assets/session/ui/fruit-background.png', ); expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '试玩' })); expect(onStartTestRun).toHaveBeenCalledWith( expect.objectContaining({ levels: [ expect.objectContaining({ uiBackgroundImageSrc: '/generated-puzzle-assets/session/ui/fruit-background.png', backgroundMusic: expect.objectContaining({ audioSrc: '/generated-puzzle-assets/session/audio/fruit.mp3', }), }), ], }), ); }); test('auto saves UI background prompt edits through levels', async () => { vi.useFakeTimers(); vi.mocked(puzzleWorksService.updatePuzzleWork).mockResolvedValue({ item: {} as never, }); render( {}} onExecuteAction={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '素材配置' })); fireEvent.change(screen.getByLabelText('拼图UI背景提示词'), { target: { value: '新的自动保存UI背景提示词' }, }); await act(async () => { await vi.runAllTimersAsync(); }); expect(puzzleWorksService.updatePuzzleWork).toHaveBeenCalledWith( 'puzzle-profile-session-1', expect.objectContaining({ levels: [ expect.objectContaining({ levelId: 'puzzle-level-1', uiBackgroundPrompt: '新的自动保存UI背景提示词', }), ], }), ); }); test('selects a history puzzle asset as reference image for the selected level', async () => { const onExecuteAction = vi.fn(); vi.mocked(puzzleAssetClient.listHistoryAssets).mockResolvedValue([ { assetObjectId: 'asset-history-1', assetKind: 'puzzle_cover_image', imageSrc: '/generated-puzzle-assets/history/image.png', ownerUserId: 'user-1', ownerLabel: '账号 user-1', profileId: null, entityId: 'puzzle-session-1', createdAt: '2026-04-27T10:00:00.000Z', updatedAt: '2026-04-27T10:00:00.000Z', }, ]); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByText('雨夜猫街')); const dialog = screen.getByRole('dialog', { name: '关卡详情' }); const uploadInput = within(dialog).getByLabelText('上传参考图', { selector: 'input', }); expect(uploadInput.closest('.platform-subpanel')).toBeTruthy(); const historyButton = within(dialog).getByRole('button', { name: '选择历史图片', }); expect(within(historyButton).getByText('历史')).toBeTruthy(); fireEvent.click(historyButton); const picker = await screen.findByRole('dialog', { name: '选择历史图片', }); fireEvent.click( await within(picker).findByRole('button', { name: /账号 user-1/u }), ); await waitFor(() => { expect(screen.queryByRole('dialog', { name: '选择历史图片' })).toBeNull(); }); fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u })); fireEvent.click( within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole( 'button', { name: '确定' }, ), ); expect(onExecuteAction).toHaveBeenLastCalledWith({ action: 'generate_puzzle_images', levelId: 'puzzle-level-1', promptText: '屋檐下的猫与暖灯街角。', referenceImageSrc: '/generated-puzzle-assets/history/image.png', imageModel: 'gpt-image-2', aiRedraw: true, candidateCount: 1, workTitle: '暖灯猫街作品', workDescription: '一套雨夜猫街主题拼图。', summary: '一套雨夜猫街主题拼图。', themeTags: ['猫咪', '雨夜', '暖灯'], levelsJson: expect.any(String), }); }); test('uses the saved level picture reference when regenerating a level image', () => { const onExecuteAction = vi.fn(); const session = createSession({ draft: { ...createSession().draft!, levels: [ { ...createSession().draft!.levels![0]!, pictureReference: '/generated-puzzle-assets/history/saved-reference.png', }, ], }, }); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByText('雨夜猫街')); fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u })); fireEvent.click( within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole( 'button', { name: '确定' }, ), ); expect(onExecuteAction).toHaveBeenCalledWith( expect.objectContaining({ action: 'generate_puzzle_images', referenceImageSrc: '/generated-puzzle-assets/history/saved-reference.png', aiRedraw: true, }), ); expect(screen.queryByPlaceholderText('参考图链接或资产ID')).toBeNull(); }); test('passes the selected image model when regenerating a level image', () => { const onExecuteAction = vi.fn(); render( {}} onExecuteAction={onExecuteAction} />, ); fireEvent.click(screen.getByText('雨夜猫街')); const dialog = screen.getByRole('dialog', { name: '关卡详情' }); fireEvent.click(within(dialog).getByRole('button', { name: '图片模型' })); fireEvent.click( within(dialog).getByRole('menuitemradio', { name: 'gpt-image-2' }), ); fireEvent.click( within(dialog).getByRole('button', { name: /重新生成画面/u }), ); fireEvent.click( within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole( 'button', { name: '确定' }, ), ); expect(onExecuteAction).toHaveBeenCalledWith( expect.objectContaining({ action: 'generate_puzzle_images', imageModel: 'gpt-image-2', }), ); }); test('shows creative agent draft edit bar and submits the current draft', () => { const onSubmit = vi.fn(); render( {}} onExecuteAction={() => {}} creativeDraftEdit={{ isBusy: false, error: null, onSubmit, }} />, ); fireEvent.change(screen.getByLabelText('智能修订拼图草稿'), { target: { value: '把标题改得轻松一点' }, }); fireEvent.click(screen.getByRole('button', { name: '修改' })); expect(onSubmit).toHaveBeenCalledWith({ instruction: '把标题改得轻松一点', currentDraft: expect.objectContaining({ workTitle: '暖灯猫街作品', workDescription: '一套雨夜猫街主题拼图。', levels: [ expect.objectContaining({ levelId: 'puzzle-level-1', pictureReference: null, }), ], }), }); }); });