a11cdf13d7
生成面板 state、重开草稿、重试身份与提交载荷统一为一份 content[] 重试身份改用 directCodexContentKey 内容指纹比对并删除 referenceIdsMatch directCodexContentToLegacyContentDto 只在任务落账的出站边界派生 账本回到草稿的反向翻译收敛为 legacyContentDtoToContent 单一实现 token 扫描从 ResourceReferenceInput 提到 resourceReferences 并两处共用 无法解析的资源引用合成 unknown 占位而不再静默丢弃 出站提示词规范化收敛为 resourceCanvasAssetGenerationPromptText 供两处共用 directCodexContentToPromptText 去掉 trim,空白只在整条 content 上判定 同步决策记录与 canonical content 里程碑落地结果
199 lines
7.3 KiB
TypeScript
199 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={{
|
|
content: [{ type: 'input_text', text: '主界面与背包页' }],
|
|
assetName: 'AI 生成 UI 设计图',
|
|
aspectRatio: '16:9',
|
|
imageSize: '1K',
|
|
}}
|
|
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',
|
|
content: [{ type: 'input_text', text: '主界面与背包页' }],
|
|
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();
|
|
});
|
|
});
|