From 367dacb57b332b52ecb834fbb6f462b8800c265c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 18 Aug 2026 14:19:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DUI=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E7=BB=93=E6=9E=9C=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI 资源路由接入真实 Tauri State store 传递项目身份并修复保存命令调用 增加合并后加载保存回归测试 --- .../src/view/project-development/index.tsx | 1 + .../src/view/ui-editor/index.tsx | 19 ++++- .../tests/uiEditorPage.test.ts | 78 ++++++++++++++++--- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 8c40d571b..afe574fc4 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -2993,6 +2993,7 @@ export default function ProjectDevelopmentView({ setUiEditorRoute(null)} /> diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx index a9946ac73..ae72e2f77 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx @@ -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( 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, diff --git a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts index 6c6168609..7acd1e907 100644 --- a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts +++ b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts @@ -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', () => {