// @vitest-environment jsdom import { cleanup, fireEvent, render, screen, waitFor, within, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, test, vi } from 'vitest'; import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView'; import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel'; import { ResourceCanvasGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasGenerationPanelView'; import { generationPromptText, typeGenerationPrompt, } from './resourceGenerationPromptTestUtils'; afterEach(() => { cleanup(); }); const uiPrototypeAction: ResourceCanvasAssetToolAction = { id: 'generate-ui-prototype', route: 'asset', label: '生成 UI 设计图', assetKind: 'ui-design', audioKind: null, assetName: 'AI 生成 UI 设计图', promptPlaceholder: '描述这张界面要承载的玩法与操作', adjustableDimensions: true, aspectRatio: '16:9', imageSize: '1K', requiresIconSpecReference: true, writesIconSpecReference: false, }; describe('图片类生成面板:点击即关闭,面板里不出现阶段文案', () => { test('点击生成同步调用提交并关闭面板,面板 DOM 里从不出现阶段 / 排队文案', async () => { const onClose = vi.fn(); const onSubmit = vi.fn(); const user = userEvent.setup(); render( , ); await typeGenerationPrompt(document.body, '主界面与背包页'); // 点击前主按钮文案就是动作名:不是阶段、也不是「已提交」。 const panel = screen.getByRole('dialog', { name: '生成 UI 设计图' }); expect( ( within(panel).getByRole('button', { name: '生成 UI 设计图', }) as HTMLButtonElement ).disabled, ).toBe(false); expect(panel.textContent).not.toMatch(/排队中。|正在生成。|提交中…/); await user.click( within(panel).getByRole('button', { name: '生成 UI 设计图' }), ); // 提交与关闭在同一个事件循环里发生:面板不等受理、不等排队、不等生成。 expect(onSubmit).toHaveBeenCalledTimes(1); expect(onClose).toHaveBeenCalledTimes(1); expect(panel.textContent).not.toMatch(/排队中。|正在生成。|提交中…/); expect(screen.queryByRole('button', { name: '后台运行并关闭' })).toBeNull(); }); test('× / Esc / 点遮罩仍能关面板,且关闭不等于取消', async () => { const onClose = vi.fn(); const onSubmit = vi.fn(); const user = userEvent.setup(); render( , ); await typeGenerationPrompt(document.body, '主界面与背包页'); await user.click( screen.getByRole('button', { name: '关闭生成 UI 设计图' }), ); expect(onClose).toHaveBeenCalledTimes(1); fireEvent.keyDown(window, { key: 'Escape' }); expect(onClose).toHaveBeenCalledTimes(2); const backdrop = document.querySelector('.fixed.inset-0') as HTMLElement; fireEvent.pointerDown(backdrop); fireEvent.pointerUp(backdrop); fireEvent.click(backdrop); expect(onClose).toHaveBeenCalledTimes(3); expect(onSubmit).not.toHaveBeenCalled(); }); test('即时失败重开时带回草稿与失败原因,改完就能重试', async () => { const onClose = vi.fn(); const onSubmit = vi.fn(); const user = userEvent.setup(); render( , ); expect(screen.getByRole('alert').textContent).toContain( '生成素材失败:远端拒绝', ); await waitFor(() => expect(generationPromptText(document.body)).toBe('主界面与背包页'), ); await user.click(screen.getByRole('button', { name: '生成 UI 设计图' })); expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit.mock.calls[0]?.[0]).toMatchObject({ kind: 'ui-design', content: [{ type: 'input_text', text: '主界面与背包页' }], assetName: 'AI 生成 UI 设计图', }); }); }); /* 音频面板没有在途状态:点「生成」即把输入交给宿主(同步入参 + 立即关闭),阶段文案只由 画布上的「生成任务」侧栏拥有。 */ describe('音频生成面板:点击即关闭,面板里不出现阶段文案', () => { test('点「生成音效」同步调用提交并关闭面板,从不出现阶段 / 后台文案', async () => { const onClose = vi.fn(); const onSubmit = vi.fn(); const user = userEvent.setup(); render( , ); await user.type(screen.getByLabelText('生成提示词'), '木门推开的声音'); const panel = screen.getByRole('dialog', { name: '生成音效' }); expect(panel.textContent).not.toMatch(/排队中。|正在生成。|提交中…/); await user.click(within(panel).getByRole('button', { name: '生成音效' })); // 提交与关闭在同一个事件循环里发生:不等受理、不等排队、不等生成。 expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit.mock.calls[0]?.[0]).toMatchObject({ kind: 'sound-effect', prompt: '木门推开的声音', assetName: '新音效', }); expect(onClose).toHaveBeenCalledTimes(1); // 进度只出现在「生成任务」侧栏:面板 DOM 里既没有阶段文案,也没有在途按钮。 expect(panel.textContent).not.toMatch(/排队中。|正在生成。|提交中…/); expect(screen.queryByRole('button', { name: '后台运行并关闭' })).toBeNull(); }); test('× / Esc 关面板只收起草稿:不提交,也不取消任何任务', async () => { const onClose = vi.fn(); const onSubmit = vi.fn(); const user = userEvent.setup(); render( , ); await user.type(screen.getByLabelText('生成提示词'), '木门推开的声音'); await user.click(screen.getByRole('button', { name: '关闭生成音效' })); expect(onClose).toHaveBeenCalledTimes(1); // 关闭把当前草稿交回宿主:用户再点开占位卡能接着编辑,而不是回到空表单。 expect(onClose.mock.calls[0]?.[0]).toMatchObject({ kind: 'sound-effect', prompt: '木门推开的声音', assetName: '新音效', }); fireEvent.keyDown(window, { key: 'Escape' }); expect(onClose).toHaveBeenCalledTimes(2); expect(onSubmit).not.toHaveBeenCalled(); }); });