Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationBackgroundClose.test.tsx
suzmii 14a0286f70 fix(agc): 生成面板点击即关闭、面板内不再出现阶段文案,只有即时失败才把草稿带回来
- ResourceCanvasAssetGenerationPanelView:删掉 `phaseDetail` 通道与整套在途状态(`submitting` /
  禁用输入 / 「后台运行并关闭」),主按钮文案恒为动作名;提交时**同步**调用 onSubmit 并 onClose,
  面板不等 IPC、不等排队、不等生成。新增 `draft` / `error` 两个入参:只有「后端从未受理」的即时
  失败重开时,宿主才把草稿与原因带回来(`role="alert"` 可见、改完即可重试)
- index.tsx:`submitResourceAssetGeneration` 改为同步返回(不再 await 终局),记录本次提交上下文
  (taskId / action / 草稿 / 是否本来就该立刻派发),提交后自动开任务面板并给一次提示条;
  收尾回调里区分两类失败:`record === null` 且本来就该立刻派发 → 重开提交面板并带回草稿;
  受理之后才失败 → 不重开面板,只在任务面板收口为失败 + 提示条
- index.tsx:删掉只为旧「在途阶段文案」服务的 `resourceAssetGenerationPanelTaskIdRef` 与成功后的
  自动收面板逻辑(面板此时已关闭)
- 测试:`resourceCanvasAssetGenerationBackgroundClose.test.tsx` 的图片类用例改写为「点击即关闭 +
  面板 DOM 里从不出现 排队中。/正在生成。/提交中…」+「即时失败重开带回草稿与原因」;音频面板用例不变
- 测试:`resourceCanvasBottomToolbar.test.tsx` 的失败重试用例按新语义改写(点击即关闭、重开时显示原因)
- 测试:AppSurface 新增三条(同步关闭且面板无阶段文案、后端未受理时重开并保留草稿、受理后失败不重开
  只在任务面板收口),并把既有生成任务 e2e 的提交步骤改为「点击即关闭」
- 顺带按仓库规范修正以上文件的 eslint import sort 与 prettier 格式(未触碰无关文件)
2026-09-14 18:35:11 +08:00

186 lines
6.4 KiB
TypeScript

// @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';
afterEach(() => {
cleanup();
});
const uiPrototypeAction: ResourceCanvasAssetToolAction = {
id: 'generate-ui-prototype',
route: 'asset',
label: '生成 UI 设计图',
assetKind: 'ui-prototype',
audioKind: null,
assetName: 'AI 生成 UI 设计图',
promptPlaceholder: '描述这张界面要承载的玩法与操作',
adjustableDimensions: true,
aspectRatio: '16:9',
imageSize: '1K',
requiresIconSpecReference: true,
writesIconSpecReference: false,
};
/** 永不 resolve 的提交:音频面板仍然等生成结束,用它模拟在途状态。 */
function pendingSubmit() {
return new Promise<void>(() => undefined);
}
describe('图片类生成面板:点击即关闭,面板里不出现阶段文案', () => {
test('点击生成同步调用提交并关闭面板,面板 DOM 里从不出现阶段 / 排队文案', async () => {
const onClose = vi.fn();
const onSubmit = vi.fn();
const user = userEvent.setup();
render(
<ResourceCanvasAssetGenerationPanelView
action={uiPrototypeAction}
onSubmit={onSubmit}
onClose={onClose}
/>,
);
await user.type(screen.getByLabelText('生成提示词'), '主界面与背包页');
// 点击前主按钮文案就是动作名:不是阶段、也不是「已提交」。
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(
<ResourceCanvasAssetGenerationPanelView
action={uiPrototypeAction}
onSubmit={onSubmit}
onClose={onClose}
/>,
);
await user.type(screen.getByLabelText('生成提示词'), '主界面与背包页');
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(
<ResourceCanvasAssetGenerationPanelView
action={uiPrototypeAction}
draft={{
prompt: '主界面与背包页',
assetName: 'AI 生成 UI 设计图',
aspectRatio: '16:9',
imageSize: '1K',
}}
error="生成素材失败:远端拒绝"
onSubmit={onSubmit}
onClose={onClose}
/>,
);
expect(screen.getByRole('alert').textContent).toContain(
'生成素材失败:远端拒绝',
);
expect(
(screen.getByLabelText('生成提示词') as HTMLTextAreaElement).value,
).toBe('主界面与背包页');
await user.click(screen.getByRole('button', { name: '生成 UI 设计图' }));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0]?.[0]).toMatchObject({
kind: 'ui-prototype',
prompt: '主界面与背包页',
assetName: 'AI 生成 UI 设计图',
});
});
});
describe('音频生成面板在提交期间可关闭', () => {
test('提交在途时点 × 能关闭面板,且不改动 pending-edit 账本语义', async () => {
const onClose = vi.fn();
const onSubmit = vi.fn(pendingSubmit);
const user = userEvent.setup();
render(
<ResourceCanvasGenerationPanelView
kinds={['sound-effect']}
initialKind="sound-effect"
onSubmit={onSubmit}
onClose={onClose}
/>,
);
await user.type(screen.getByLabelText('生成提示词'), '木门推开的声音');
await user.click(screen.getByRole('button', { name: '生成音效' }));
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
const closeButton = screen.getByRole('button', {
name: '关闭生成音效',
}) as HTMLButtonElement;
expect(closeButton.disabled).toBe(false);
await user.click(closeButton);
expect(onClose).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledTimes(1);
fireEvent.keyDown(window, { key: 'Escape' });
expect(onClose).toHaveBeenCalledTimes(2);
});
test('提交在途时提供「后台运行并关闭」', async () => {
const onClose = vi.fn();
const user = userEvent.setup();
render(
<ResourceCanvasGenerationPanelView
kinds={['sound-effect']}
initialKind="sound-effect"
onSubmit={vi.fn(pendingSubmit)}
onClose={onClose}
/>,
);
await user.type(screen.getByLabelText('生成提示词'), '木门推开的声音');
await user.click(screen.getByRole('button', { name: '生成音效' }));
await user.click(screen.getByRole('button', { name: '后台运行并关闭' }));
expect(onClose).toHaveBeenCalledTimes(1);
});
});