Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanel.test.tsx
T
suzmii 13b28ebbc7 生成占位与卡下独立浮层,修复引用选择被焦点陷阱阻断
工具点击先在当前栏目创建临时占位卡,生成浮层挂在占位下沿;图片、音效、背景音乐入口一致
占位按项目与草稿 ID 归属宿主临时状态:可拖动、提交绑定任务 ID、成功结果落到占位最新位置
删除占位只隐藏展示,关闭浮层不冒充取消后台任务;失败保留占位与输入引用供重试
生成面板改为非模态浮层:ThemedModal 焦点陷阱曾阻断 portal 到 body 的引用选择器,点选不生效
引用选择器定位加顶边钳制,搜索收窄与键盘选择可用;不再越出视口
参考候选与陈旧引用校验排除 SVG(原生解码不支持),其余 image 类型不额外收窄
任务创建处对参考资产 ID 去重;补浮层真实组件整合、占位模型与拖动、参考模型回归测试
2026-09-17 18:22:39 +08:00

160 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
import {
cleanup,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { GameCreationAppAssetManifestEntry } from '../../../packages/shared/src/contracts/gameCreationApp';
import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView';
import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel';
import { typeGenerationPrompt } from './resourceGenerationPromptTestUtils';
afterEach(cleanup);
const imageAction: ResourceCanvasAssetToolAction = {
id: 'generate-image',
route: 'asset',
label: '生成图片',
assetKind: 'image',
audioKind: null,
assetName: 'AI 生成图片',
promptPlaceholder: '今天想生成什么画面?',
adjustableDimensions: true,
aspectRatio: '1:1',
imageSize: '1K',
requiresIconSpecReference: false,
writesIconSpecReference: false,
};
function asset(
id: string,
label: string,
mediaType = 'image/png',
): GameCreationAppAssetManifestEntry {
return {
id,
kind: 'image',
mediaType,
localPath: `assets/${label}.png`,
source: { kind: 'canvas' },
};
}
/** 取面板里的引用输入区(含 `@` 触发器按钮),把操作局限在它自己身上。 */
function referenceInputScope(ariaLabel: string) {
const root = screen.getByLabelText(ariaLabel).closest('.resource-reference-input');
if (!root) {
throw new Error(`资源引用输入区不存在:${ariaLabel}`);
}
return within(root as HTMLElement);
}
describe('生成浮层里的引用选择(真实组件,不做 props mock)', () => {
test('浮层不是模态,真实点击候选就能选中并随提交带走引用', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
const assets = [
asset('asset-a', '素材-a'),
asset('asset-b', '素材-b'),
];
render(
<ResourceCanvasAssetGenerationPanelView
action={imageAction}
variant="floating"
assets={assets}
projectPath="/tmp/project"
onSubmit={onSubmit}
onClose={() => undefined}
/>,
);
const panel = screen.getByRole('dialog', { name: '生成图片' });
/*
浮层形态不是模态:没有全屏遮罩、没有 aria-modal。`ThemedModal` 的焦点陷阱正是 P1 的成因——
引用选择器 portal 到 body,被陷阱挡在外面时候选项点了不生效(真实浏览器复现过)。
*/
expect(panel.getAttribute('aria-modal')).toBeNull();
expect(document.querySelector('[aria-modal="true"]')).toBeNull();
expect(document.querySelector('.fixed.inset-0')).toBeNull();
await typeGenerationPrompt(panel, '画一只猫');
await user.click(
referenceInputScope('生成提示词').getByRole('button', {
name: '插入素材引用',
}),
);
const picker = await screen.findByRole('dialog', { name: '选择素材' });
await user.click(
within(picker).getByRole('option', { name: /素材-a/ }),
);
expect(within(picker).getByText('已选择 1 个')).not.toBeNull();
const insert = within(picker).getByRole('button', { name: '插入引用' });
expect((insert as HTMLButtonElement).disabled).toBe(false);
await user.click(insert);
// 选中的引用进了面板:计数可见、提交时随载荷带走。
await waitFor(() =>
expect(within(panel).getByText('参考图 1/5')).not.toBeNull(),
);
await user.click(within(panel).getByRole('button', { name: '生成图片' }));
expect(onSubmit).toHaveBeenCalledTimes(1);
const submitted = onSubmit.mock.calls[0]?.[0] as {
prompt: string;
references: { resourceId: string }[];
};
expect(submitted.references.map((reference) => reference.resourceId)).toEqual(
['asset-a'],
);
expect(submitted.prompt).toContain('画一只猫');
});
test('搜索能收窄候选,键盘也能完成选择', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
const assets = [
asset('asset-a', '素材-a'),
asset('asset-b', '素材-b'),
];
render(
<ResourceCanvasAssetGenerationPanelView
action={imageAction}
variant="floating"
assets={assets}
projectPath="/tmp/project"
onSubmit={onSubmit}
onClose={() => undefined}
/>,
);
const panel = screen.getByRole('dialog', { name: '生成图片' });
await typeGenerationPrompt(panel, '画一只猫');
await user.click(
referenceInputScope('生成提示词').getByRole('button', {
name: '插入素材引用',
}),
);
const picker = await screen.findByRole('dialog', { name: '选择素材' });
expect(within(picker).getAllByRole('option')).toHaveLength(2);
// 搜索收窄:只剩「素材-b」这一条候选。
await user.clear(screen.getByLabelText('搜索全部画布素材'));
await user.type(screen.getByLabelText('搜索全部画布素材'), '素材-b');
await waitFor(() =>
expect(within(picker).getAllByRole('option')).toHaveLength(1),
);
// 键盘路径:Tab 进候选列表,Enter 选中(不依赖鼠标点击)。
const option = within(picker).getByRole('option', { name: /素材-b/ });
option.focus();
await user.keyboard('{Enter}');
await waitFor(() =>
expect(within(picker).getByText('已选择 1 个')).not.toBeNull(),
);
});
});