Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanel.test.tsx
T
k88936 394b8de9d2 重构:DirectProject 回合错误去前缀、拆分 wire 模块、终态改判别联合
- 三条错误表去冗余前缀:DirectTurnError→TurnError、DirectTurnEnqueueError→EnqueueError、DirectTurnFailure→TurnFailure、DirectTurnDeadline→Deadline、DirectCodexFailureStage→FailureStage、DirectCodexNativeKind→NativeKind、DirectModelCallKind→ModelCallKind
- 用户条目去前缀:DirectCodexUserItem→UserItem 等六个类型;agent/direct_turn_error.rs 移到 codex_app_server/turn_error.rs
- thread_manager/wire.rs 拆为 wire/{mod,clock,items,turn,failure,tests}.rs;direct_turn_failure.rs 的失败载荷并入 wire/failure.rs
- 宿主侧回合终态从 struct TurnTerminal 改为 enum TurnCompletion{Completed,Interrupted,Aborted,Failed},从 wire 移到 thread_manager/turn_completion.rs,status 由变体反推
- 过期回合释放兜底改走 TurnCompletion::Aborted.event
- 同步生成绑定、前端调用点与测试;修正终态单测里过时的 TransportClosed 断言
- 对 src-tauri 全量 cargo fmt,满足 pre-commit 的 rustfmt 门禁
2026-10-02 18:10:17 +08:00

168 lines
5.9 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 {
directCodexContentToPromptText,
resourceLabelResolver,
} from '../src/features/project-workspace/resourceReferences';
import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView';
import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel';
import type { UserContentPart } from '../src/view/project-development/chat/generated/UserContentPart';
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 {
content: UserContentPart[];
};
// 提交载荷只有一份 canonical content:正文逐字,引用仍是稳定 resourceId。
expect(
directCodexContentToPromptText(
submitted.content,
resourceLabelResolver(assets),
),
).toContain('画一只猫');
expect(
submitted.content.some(
(part) =>
part.type === 'agc_resource_reference' &&
part.resourceId === 'asset-a',
),
).toBe(true);
});
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(),
);
});
});