Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationBackgroundClose.test.tsx
T
k88936 99fb6c38c1 AGC 音频生成并入图片类那份后台任务账本
- 原生:`start_local_project_asset_generation` 新增可选入参 `idempotencyKey`,并在音频 kind 上分叉;图片类载荷与分支逐字未改
- 原生:新增音频提交期收口 `prepare_local_project_audio_generation`(kind / 提示词上限 / 素材名 / operation 身份 / 幂等键)
- 原生:新增 `run_local_project_audio_generation_at`,在派发时刻读项目 revision 并复用既有音频无源生成实现,不复制生成逻辑
- 原生:新增 `begin_local_project_audio_generation_task` / `run_local_project_audio_generation_task`,音频任务落同一份项目内账本并写 running → completed / failed,跑完没登记素材按失败收口
- 原生:补两条用例——被拒绝的提交零写入账本、音频任务落在同一账本且 kind 正确
- 前端任务模型:新增音频任务与 `idempotencyKey` 字段,恢复出来的历史任务不带它也不承接重试,入口文案扩到音频栏目
- 前端队列:按 kind 分流派发载荷,音频只发任务身份(任务 id 即 operation id)与幂等键
- 前端面板:音频生成面板改为点「生成」同步提交并立即关闭,删除「生成中…」「后台运行并关闭」与输入锁定
- 前端宿主:音频提交改为同步入队并展开「生成任务」侧栏,只有「后端从未受理」才连原草稿与原请求身份重开面板
- 前端清理:删除随本次改动失效的 `resourceCanvasGenerationSourceId` 与宿主里不再使用的 import
- 测试:面板 / 队列 / 宿主生命周期 / 落点 / appSurface 改按后台账本口径断言,并补「未受理即时失败重开」用例(已用移除重开逻辑的变异验证其非空)
- 文档:PRD §3.10 / §7.9、AGC 底部工具栏入口矩阵、V3 端到端验收 S11a 同步为音频后台化口径
- 文档:新增里程碑与实施计划(含验收证据矩阵),并在 decision-log 记下「音频并入后台任务账本」这条长期约定
2026-09-20 23:32:09 +08:00

200 lines
7.3 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';
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(
<ResourceCanvasAssetGenerationPanelView
action={uiPrototypeAction}
onSubmit={onSubmit}
onClose={onClose}
/>,
);
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(
<ResourceCanvasAssetGenerationPanelView
action={uiPrototypeAction}
onSubmit={onSubmit}
onClose={onClose}
/>,
);
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(
<ResourceCanvasAssetGenerationPanelView
action={uiPrototypeAction}
draft={{
prompt: '主界面与背包页',
assetName: 'AI 生成 UI 设计图',
aspectRatio: '16:9',
imageSize: '1K',
references: [],
}}
error="生成素材失败:远端拒绝"
onSubmit={onSubmit}
onClose={onClose}
/>,
);
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',
prompt: '主界面与背包页',
assetName: 'AI 生成 UI 设计图',
});
});
});
/* 音频面板没有在途状态:点「生成」即把输入交给宿主(同步入参 + 立即关闭),阶段文案只由
画布上的「生成任务」侧栏拥有。 */
describe('音频生成面板:点击即关闭,面板里不出现阶段文案', () => {
test('点「生成音效」同步调用提交并关闭面板,从不出现阶段 / 后台文案', async () => {
const onClose = vi.fn();
const onSubmit = vi.fn();
const user = userEvent.setup();
render(
<ResourceCanvasGenerationPanelView
kinds={['sound-effect']}
initialKind="sound-effect"
onSubmit={onSubmit}
onClose={onClose}
/>,
);
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(
<ResourceCanvasGenerationPanelView
kinds={['sound-effect']}
initialKind="sound-effect"
onSubmit={onSubmit}
onClose={onClose}
/>,
);
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();
});
});