修复UI编辑器合并结果持久化

UI 资源路由接入真实 Tauri State store

传递项目身份并修复保存命令调用

增加合并后加载保存回归测试
This commit is contained in:
2026-08-18 14:19:35 +08:00
parent d04b79501e
commit 367dacb57b
3 changed files with 85 additions and 13 deletions
@@ -2993,6 +2993,7 @@ export default function ProjectDevelopmentView({
<UiEditorPage
projectPath={projectPath}
resourceId={uiEditorRoute.resourceId}
projectId={manifest.projectId}
resourceLabel={uiEditorRoute.resourceLabel}
onBack={() => setUiEditorRoute(null)}
/>
@@ -1,8 +1,9 @@
import { ChevronLeft } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ThemedModal } from '../../components/modal/ThemedModal';
import {
createTauriUiDesignStateStore,
mockUiDesignStateStore,
type UiDesignStateStore,
} from '../../features/ui-editor/uiDesignStateStore';
@@ -21,17 +22,27 @@ import {
export default function UiEditorPage({
projectPath,
resourceId,
projectId,
resourceLabel,
onBack,
stateStore = mockUiDesignStateStore,
stateStore,
}: {
projectPath: string;
resourceId?: string;
projectId?: string;
resourceLabel?: string;
onBack?: () => void;
stateStore?: UiDesignStateStore;
}) {
const controller = useUiEditorPage(projectPath, resourceId, stateStore);
const resolvedStore = useMemo(
() =>
stateStore ??
(resourceId && projectId
? createTauriUiDesignStateStore(projectPath, projectId)
: mockUiDesignStateStore),
[projectId, projectPath, resourceId, stateStore],
);
const controller = useUiEditorPage(projectPath, resourceId, resolvedStore);
const [savedStateSignature, setSavedStateSignature] = useState<string | null>(
null,
);
@@ -73,7 +84,7 @@ export default function UiEditorPage({
const snapshot = structuredClone(controller.editor.state);
const snapshotSignature = JSON.stringify(snapshot);
try {
const result = await stateStore.save(
const result = await resolvedStore.save(
resourceId,
controller.persistedRevision,
snapshot,
@@ -6,10 +6,13 @@ import {
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,
@@ -20,19 +23,81 @@ vi.mock('../src/components/modal/ThemedModal', () => ({
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 编辑工具' }),
screen.getByRole('navigation', { name: 'UI 编辑流程' }),
).toBeTruthy();
expect(screen.getByRole('button', { name: '导入界面图' })).toBeTruthy();
expect(screen.getByRole('button', { name: '导入独立素材' })).toBeTruthy();
expect(screen.getByRole('button', { name: '导入字体' })).toBeTruthy();
expect(screen.getByText('从界面图开始')).toBeTruthy();
expect(screen.queryByText('Pause Dialog')).toBeNull();
});
@@ -40,13 +105,8 @@ describe('UiEditorPage', () => {
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();
expect(screen.getByText('当前界面尚无组件树。')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '检查前置数据' }));
expect(screen.getByText('还不能执行此操作')).toBeTruthy();
expect(screen.getByText('请先导入界面图')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /绑定视觉素材/ }));
expect(screen.getByRole('heading', { name: '检查发现问题' })).toBeTruthy();
});
it('opens the design-image importer independently', () => {