2cf3183e78
概览重复定位时重新触发状态提示动画 为待审与阻塞状态提供差异化呼吸高亮 补充状态高亮令牌的回归测试
382 lines
13 KiB
TypeScript
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);
|
|
});
|
|
});
|