18252e24b8
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
合并 origin/master 的 AGC 画布参考与项目能力更新 解决 canvas 生成恢复与任务模型冲突并继续使用共享 GameCreationAppAssetKind 保留 run_id、generationKind 与外部协议字符串的独立边界
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
// @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(),
|
||
);
|
||
});
|
||
});
|