Files
Genarrative/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts
T
k88936 2cf3183e78 增强UI编辑器状态高亮动效
概览重复定位时重新触发状态提示动画

为待审与阻塞状态提供差异化呼吸高亮

补充状态高亮令牌的回归测试
2026-08-19 13:14:44 +08:00

382 lines
13 KiB
TypeScript

// @vitest-environment jsdom
import {
act,
fireEvent,
render,
renderHook,
screen,
waitFor,
} from '@testing-library/react';
import { createElement, type ReactNode } from 'react';
import { describe, expect, it, vi } from 'vitest';
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }));
vi.mock('../src/components/AssetImporter', () => ({
AssetImporter: ({ open }: { open: boolean }) =>
open ? createElement('div', { role: 'dialog' }, '素材导入器') : null,
}));
vi.mock('../src/components/modal/ThemedModal', () => ({
ThemedModal: ({ open, children }: { open: boolean; children: ReactNode }) =>
open ? createElement('div', { role: 'dialog' }, children) : null,
}));
import { invoke } from '@tauri-apps/api/core';
import UiEditorPage from '../src/view/ui-editor';
import { useUiEditorPage } from '../src/view/ui-editor/useUiEditorPage';
describe('UiEditorPage', () => {
it('uses the durable Tauri store for a project UI resource', async () => {
vi.mocked(invoke)
.mockResolvedValueOnce({
revision: 0,
state: {
ui_trees: [],
ui_design_images: {},
sprite_assets: {},
font_assets: {},
},
})
.mockResolvedValueOnce({
status: 'saved',
state: {
ui_trees: [],
ui_design_images: {},
sprite_assets: {},
font_assets: {},
},
revision: 1,
committedProjectRevision: 1,
});
render(
createElement(UiEditorPage, {
projectPath: '/tmp/ui-editor-project',
projectId: 'local-project-draft',
resourceId: 'generated-ui-1',
} as never),
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('load_ui_design_state', {
input: {
projectPath: '/tmp/ui-editor-project',
expectedProjectId: 'local-project-draft',
assetId: 'generated-ui-1',
},
});
});
fireEvent.click(screen.getByRole('button', { name: '保存' }));
fireEvent.click(screen.getByRole('button', { name: '仍然保存' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('save_ui_design_state', {
input: {
projectPath: '/tmp/ui-editor-project',
expectedProjectId: 'local-project-draft',
assetId: 'generated-ui-1',
expectedRevision: 0,
state: {
ui_trees: [],
ui_design_images: {},
sprite_assets: {},
font_assets: {},
},
},
});
});
});
it('renders split input tools over a real empty State', () => {
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
expect(
screen.getByRole('navigation', { name: 'UI 编辑流程' }),
).toBeTruthy();
expect(screen.getByRole('button', { name: '导入界面图' })).toBeTruthy();
expect(screen.getByRole('button', { name: '导入独立素材' })).toBeTruthy();
expect(screen.getByText('从界面图开始')).toBeTruthy();
expect(screen.queryByText('Pause Dialog')).toBeNull();
});
it('renders the overview that belongs to the active workflow stage', () => {
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
expect(screen.getByRole('heading', { name: '导入概览' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /识别界面结构/ }));
fireEvent.click(screen.getByRole('button', { name: '仍然继续' }));
expect(screen.getByRole('heading', { name: '识别概览' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /绑定视觉素材/ }));
fireEvent.click(screen.getByRole('button', { name: '仍然继续' }));
expect(screen.getByRole('heading', { name: '绑定概览' })).toBeTruthy();
});
it('keeps the pending binding count informational instead of navigable', () => {
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
fireEvent.click(screen.getByRole('button', { name: /绑定视觉素材/ }));
fireEvent.click(screen.getByRole('button', { name: '仍然继续' }));
expect(
screen.queryByRole('button', { name: /待处理.*定位下一项/ }),
).toBeNull();
});
it('switches tools freely without inventing completed workflow state', () => {
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
fireEvent.click(screen.getByRole('button', { name: /绑定视觉素材/ }));
expect(screen.getByRole('heading', { name: '检查发现问题' })).toBeTruthy();
});
it('opens the design-image importer independently', () => {
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
fireEvent.click(screen.getByRole('button', { name: '导入界面图' }));
expect(screen.getByText('素材导入器')).toBeTruthy();
});
it('switches from a selected node to the sprite inspector', () => {
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
act(() => {
result.current.editor.addDesignImages([
{
id: 'page',
image: {
metadata: {
name: 'Page',
description: '',
role: 'Page',
slave_to: null,
},
path: 'assets/page.png',
pixel_size: [320, 180],
pixels_per_unit: 1,
},
},
]);
result.current.editor.addSpriteAssets([
{
asset_id: 'panel',
metadata: { name: 'Panel', asset_type: '' },
path: 'assets/panel.png',
pixel_size: [32, 32],
pixels_per_unit: 1,
border: { left: 0, right: 0, top: 0, bottom: 0 },
},
]);
result.current.editor.addFontAssets([
{
asset_id: 'body',
metadata: {
family_name: '测试字体',
face_name: 'Regular',
weight: 400,
italic: false,
format: 'TrueType',
source_file_name: 'body.ttf',
},
path: 'assets/fonts/body.ttf',
content_sha256: 'a'.repeat(64),
},
]);
});
const rootId = result.current.editor.state.ui_trees[0]!.root.id;
act(() => {
result.current.selectDesignImage('page');
result.current.selectNode(rootId);
});
expect(result.current.selectedNodeId).toBe(rootId);
act(() => result.current.selectSprite('panel'));
expect(result.current.selectedNodeId).toBeNull();
expect(result.current.selectedSpriteId).toBe('panel');
act(() => result.current.selectFont('body'));
expect(result.current.selectedFontId).toBe('body');
act(() => result.current.selectDesignImage('page'));
expect(result.current.selectedFontId).toBeNull();
act(() => result.current.selectFont('body'));
act(() => result.current.selectSprite('panel'));
expect(result.current.selectedFontId).toBeNull();
act(() => result.current.selectFont('body'));
act(() => result.current.selectNode(rootId));
expect(result.current.selectedFontId).toBeNull();
});
it('keeps node preview visibility transient and attached to global node IDs', () => {
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
act(() => {
result.current.editor.addDesignImages([
{
id: 'page-a',
image: {
metadata: {
name: 'Page A',
description: '',
role: 'Page',
slave_to: null,
},
path: 'assets/page-a.png',
pixel_size: [320, 180],
pixels_per_unit: 1,
},
},
{
id: 'page-b',
image: {
metadata: {
name: 'Page B',
description: '',
role: 'Page',
slave_to: null,
},
path: 'assets/page-b.png',
pixel_size: [320, 180],
pixels_per_unit: 1,
},
},
]);
});
act(() => result.current.selectDesignImage('page-a'));
const rootId = result.current.treeForActiveImage!.root.id;
let insertedNodeId: string | undefined;
act(() => {
const inserted = result.current.insertNode(rootId);
if (inserted?.ok) insertedNodeId = inserted.value;
});
const stateBeforeToggle = JSON.stringify(result.current.editor.state);
act(() => {
result.current.toggleNodePreviewVisibility(rootId);
});
expect(result.current.isNodePreviewVisible(rootId)).toBe(false);
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
expect(JSON.stringify(result.current.editor.state)).toBe(stateBeforeToggle);
act(() => result.current.selectDesignImage('page-b'));
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
act(() => result.current.selectDesignImage('page-a'));
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
expect(insertedNodeId).toBeTruthy();
act(() => result.current.toggleNodePreviewVisibility(insertedNodeId!));
expect(result.current.hiddenNodeIds.has(insertedNodeId!)).toBe(true);
act(() => result.current.deleteNode(insertedNodeId!));
expect(result.current.hiddenNodeIds.has(insertedNodeId!)).toBe(false);
act(() => result.current.clearState());
expect(result.current.hiddenNodeIds.size).toBe(0);
});
it('keeps Inspector status highlighting separate from node navigation', () => {
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
act(() => {
result.current.editor.addDesignImages([
{
id: 'page',
image: {
metadata: {
name: 'Page',
description: '',
role: 'Page',
slave_to: null,
},
path: 'assets/page.png',
pixel_size: [320, 180],
pixels_per_unit: 1,
},
},
]);
result.current.selectDesignImage('page');
});
const rootId = result.current.treeForActiveImage!.root.id;
let otherNodeId: string | undefined;
act(() => {
otherNodeId = result.current.insertNode(rootId)?.value;
result.current.selectNode(rootId);
result.current.highlightStatusField('layout_status');
});
expect(result.current.highlightedStatusField).toMatchObject({
field: 'layout_status',
requestId: 1,
});
act(() => result.current.highlightStatusField('layout_status'));
expect(result.current.highlightedStatusField).toMatchObject({
field: 'layout_status',
requestId: 2,
});
act(() => result.current.selectNode(otherNodeId!));
expect(result.current.highlightedStatusField).toBeNull();
act(() => {
result.current.highlightStatusField('components_status');
result.current.setNodeMetadata({ components_status: 'Passed' });
});
expect(result.current.highlightedStatusField).toBeNull();
});
it('shares exclusive child visibility between tree actions and final preview state', () => {
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
act(() => {
result.current.editor.addDesignImages([
{
id: 'page',
image: {
metadata: {
name: 'Page',
description: '',
role: 'Page',
slave_to: null,
},
path: 'assets/page.png',
pixel_size: [320, 180],
pixels_per_unit: 1,
},
},
]);
result.current.selectDesignImage('page');
});
const rootId = result.current.treeForActiveImage!.root.id;
let parentId: string | undefined;
let childAId: string | undefined;
let childBId: string | undefined;
let childBDescendantId: string | undefined;
act(() => {
parentId = result.current.insertNode(rootId)?.value;
childAId = result.current.insertNode(parentId!)?.value;
childBId = result.current.insertNode(parentId!)?.value;
childBDescendantId = result.current.insertNode(childBId!)?.value;
});
act(() => result.current.selectNode(parentId!));
act(() => result.current.setNodeChildrenDisplayMode('Exclusive'));
expect(parentId && childAId && childBId).toBeTruthy();
expect(result.current.isNodePreviewVisible(childAId!)).toBe(true);
expect(result.current.isNodePreviewVisible(childBId!)).toBe(false);
expect(result.current.isNodePreviewVisible(childBDescendantId!)).toBe(
false,
);
act(() => result.current.toggleNodePreviewVisibility(childAId!));
expect(result.current.isNodePreviewVisible(childAId!)).toBe(false);
expect(result.current.isNodePreviewVisible(childBId!)).toBe(false);
act(() => result.current.toggleNodePreviewVisibility(childBId!));
expect(result.current.isNodePreviewVisible(childAId!)).toBe(false);
expect(result.current.isNodePreviewVisible(childBId!)).toBe(true);
expect(result.current.isNodePreviewVisible(childBDescendantId!)).toBe(true);
});
});