接入项目 UI 设计资源

新增 UI 设计资源的 manifest 登记与资源画布入口

将 UI 编辑器接入项目工作台并提供保存返回交互

补充 UI State mock 存储、资源投影与定向测试
This commit is contained in:
2026-08-17 13:53:13 +08:00
parent 28a63eaf88
commit 48254855e9
11 changed files with 478 additions and 22 deletions
@@ -1252,6 +1252,64 @@ pub(crate) fn register_local_asset(
)
}
#[tauri::command]
pub(crate) fn create_ui_design_resource(
project_path: String,
expected_project_id: String,
) -> Result<CreateUiDesignResourceResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
let _lock = acquire_project_write_lock(root, "asset.register")?;
let manifest = read_existing_manifest_for_project(root)?;
if manifest.project_id != expected_project_id.trim() {
return Err("project-identity-conflict".to_string());
}
let next_index = manifest
.assets
.iter()
.filter(|asset| asset.kind == "UI")
.count()
+ 1;
let resource_name = format!("UI 设计 {next_index}");
let relative_path = format!("ui/{resource_name}.json");
let absolute_path = resolve_local_project_path(root, &relative_path)?;
if let Some(parent) = absolute_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建 UI 资源目录失败:{}: {error}", parent.display()))?;
}
if !absolute_path.exists() {
fs::write(&absolute_path, "{}")
.map_err(|error| format!("创建 UI 资源失败:{}: {error}", absolute_path.display()))?;
}
advance_agent_runtime_project_revision_locked(root)?;
let asset = register_local_asset_at(
root,
&relative_path,
"UI",
"application/json",
"generated",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
canvas_project_id: None,
resource_id: Some(format!("ui:{next_index}")),
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
},
)?;
let manifest = read_existing_manifest_for_project(root)?;
let revision = read_game_creator_agent_runtime_project_revision(root)?.revision;
Ok(CreateUiDesignResourceResult {
asset,
manifest,
committed_project_revision: revision,
})
}
#[tauri::command]
pub(crate) async fn derive_local_project_resource(
input: DeriveLocalProjectResourceInput,
@@ -932,6 +932,14 @@ struct UploadLocalAssetResult {
manifest_path: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct CreateUiDesignResourceResult {
asset: UploadLocalAssetResult,
manifest: GameCreationAppManifest,
committed_project_revision: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ImportCanvasExportResult {
@@ -2261,6 +2269,7 @@ fn main() {
read_game_creator_mcp_catalog,
upload_local_asset,
register_local_asset,
create_ui_design_resource,
derive_local_project_resource,
list_pending_local_project_resource_edits,
resume_local_project_resource_edit,
@@ -0,0 +1,32 @@
import type { State } from './types/State';
import { EMPTY_UI_EDITOR_STATE } from './useUiEditorState';
export type UiDesignStateStore = {
load(resourceId: string): Promise<State>;
save(resourceId: string, state: State): Promise<void>;
};
function emptyState(): State {
return {
ui_trees: [],
ui_design_images: {},
sprite_assets: {},
font_assets: {},
};
}
/**
* TODO(ui-design-persistence): replace this adapter with project-file backed
* State serialization at the UI resource's registered localPath.
*/
export const mockUiDesignStateStore: UiDesignStateStore = {
async load(_resourceId) {
return emptyState();
},
async save(_resourceId, _state) {
// The resource and its manifest entry are durable. UI State persistence is
// deliberately deferred until the file format is ready.
},
};
export const EMPTY_UI_DESIGN_STATE = EMPTY_UI_EDITOR_STATE;
@@ -61,6 +61,7 @@ import {
LocalGamePreviewFrame,
resolveEmbeddedPreviewUrl,
} from '../../features/project-workspace/LocalGamePreviewFrame';
import UiEditorPage from '../ui-editor';
import {
type ProjectManifestSnapshotMetadata,
resolveResourceFocusIntent,
@@ -152,6 +153,17 @@ type ResourceEditorRoute = {
capability: Extract<ProjectResourceEditCapability, { route: 'derive' }>;
};
type UiEditorRoute = {
resourceId: string;
resourceLabel: string;
};
type CreateUiDesignResourceResult = {
asset: { id: string };
manifest: GameCreationAppManifest;
committedProjectRevision: number;
};
type DeriveLocalProjectResourceResult = {
operationId: string;
sourceResourceId: string;
@@ -693,6 +705,9 @@ export default function ProjectDevelopmentView({
useState<AssetCanvasRoute | null>(null);
const [resourceEditorRoute, setResourceEditorRoute] =
useState<ResourceEditorRoute | null>(null);
const [uiEditorRoute, setUiEditorRoute] = useState<UiEditorRoute | null>(
null,
);
const [assetCanvasNotice, setAssetCanvasNotice] = useState('');
const [pendingResourceEdits, setPendingResourceEdits] = useState<
PendingLocalProjectResourceEdit[]
@@ -1586,6 +1601,7 @@ export default function ProjectDevelopmentView({
pendingResourceFocusRef.current = null;
setAssetCanvasRoute(null);
setResourceEditorRoute(null);
setUiEditorRoute(null);
resourceEditorRevisionRef.current.clear();
setHiddenCommittedResourceId(null);
setPendingResourceEdits([]);
@@ -1973,6 +1989,14 @@ export default function ProjectDevelopmentView({
const handleResourceSelect = useCallback(
(resourceId: string) => {
const resource = resources.find((entry) => entry.id === resourceId);
if (resource?.subtype === 'UI' && resource.manifestAssetId !== null) {
setUiEditorRoute({
resourceId: resource.manifestAssetId,
resourceLabel: resource.label,
});
return;
}
advanceFocusGeneration();
stopActiveCardMedia();
captureResourceSectionScrollPositions();
@@ -1987,10 +2011,39 @@ export default function ProjectDevelopmentView({
advanceFocusGeneration,
captureResourceListScrollPosition,
captureResourceSectionScrollPositions,
resources,
stopActiveCardMedia,
],
);
const createUiDesignResource = useCallback(async () => {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
setAssetCanvasNotice('UI 设计资源需要在客户端内创建');
return;
}
setAssetCanvasNotice('正在创建 UI 设计…');
try {
const result = await invoke<CreateUiDesignResourceResult>(
'create_ui_design_resource',
{ projectPath, expectedProjectId: manifest.projectId },
);
if (result.manifest.projectId !== manifest.projectId) {
throw new Error('UI 设计资源登记结果与当前项目不一致');
}
onManifestChange?.(projectPath, result.manifest, {
projectId: result.manifest.projectId,
revision: result.committedProjectRevision,
source: 'asset-command',
});
setAssetCanvasNotice('UI 设计已创建,正在同步资源与布局…');
} catch (error) {
setAssetCanvasNotice(
error instanceof Error ? error.message : String(error),
);
}
}, [manifest.projectId, onManifestChange, projectPath]);
function showResourceSortMode(nextMode: ResourceSortMode) {
if (nextMode === sortMode) {
return;
@@ -2906,6 +2959,27 @@ export default function ProjectDevelopmentView({
setMode('run');
}
const resourceViewState = (() => {
switch (mode) {
case 'resources':
if (assetCanvasRoute) {
return `resources.asset-canvas.${assetCanvasRoute.scope.intent}`;
}
if (resourceEditorRoute) {
return `resources.editor.${resourceEditorRoute.capability.editKind}`;
}
if (uiEditorRoute) {
return 'resources.ui-editor';
}
if (focusedResource) {
return `resources.focused.${focusedResource.category}`;
}
return 'resources.list';
case 'run':
return undefined;
}
})();
return (
<section
className="launcher-page launcher-project-development game-project-workbench"
@@ -2915,17 +2989,7 @@ export default function ProjectDevelopmentView({
<section
className="game-workbench-stage"
aria-label="项目主视窗"
data-resource-view-state={
mode === 'resources'
? assetCanvasRoute
? `resources.asset-canvas.${assetCanvasRoute.scope.intent}`
: resourceEditorRoute
? `resources.editor.${resourceEditorRoute.capability.editKind}`
: focusedResource
? `resources.focused.${focusedResource.category}`
: 'resources.list'
: undefined
}
data-resource-view-state={resourceViewState}
>
<div className="game-workbench-toolbar">
<div className="game-workbench-tabs" role="tablist">
@@ -2959,7 +3023,8 @@ export default function ProjectDevelopmentView({
{mode === 'resources' &&
!focusedResource &&
!assetCanvasRoute &&
!resourceEditorRoute ? (
!resourceEditorRoute &&
!uiEditorRoute ? (
<>
{pendingResourceEdits.length > 0 ||
pendingResourceEditsLoadState === 'failed' ? (
@@ -2983,6 +3048,14 @@ export default function ProjectDevelopmentView({
<Sparkles size={15} aria-hidden="true" />
</button>
{/*// TODO this is only for debug usage, should integrate to the new asset above later*/}
<button
type="button"
onClick={() => void createUiDesignResource()}
>
<Sparkles size={15} aria-hidden="true" />
UI
</button>
<button
type="button"
className={sortMode === 'dependency' ? 'is-active' : ''}
@@ -3016,7 +3089,8 @@ export default function ProjectDevelopmentView({
{!runAvailable &&
!focusedResource &&
!assetCanvasRoute &&
!resourceEditorRoute ? (
!resourceEditorRoute &&
!uiEditorRoute ? (
<p
id="run-unavailable-hint"
className="game-run-unavailable"
@@ -3026,7 +3100,16 @@ export default function ProjectDevelopmentView({
</p>
) : null}
{mode === 'resources' && assetCanvasRoute ? (
{/* TODO(ui-design-surface): decide whether the workbench toolbar is
hidden for the UI editor surface as part of a dedicated layout pass. */}
{mode === 'resources' && uiEditorRoute ? (
<UiEditorPage
projectPath={projectPath}
resourceId={uiEditorRoute.resourceId}
resourceLabel={uiEditorRoute.resourceLabel}
onBack={() => setUiEditorRoute(null)}
/>
) : mode === 'resources' && assetCanvasRoute ? (
<AssetCanvasSurface
host={assetCanvasRoute.host}
scope={assetCanvasRoute.scope}
@@ -107,6 +107,12 @@ const videoExtension = /\.(mp4|webm|mov)$/iu;
export function projectResourceCardPreviewKind(
resource: ProjectResource,
): ProjectResourceCardPreviewKind {
// TODO(ui-design-preview): render the persisted UI Tree once UI State is
// stored at the resource path; the registered resource currently uses the
// normal placeholder visual.
if (resource.subtype === 'UI') {
return 'placeholder';
}
if (resource.category === 'version') {
return 'version';
}
@@ -76,6 +76,7 @@ export function classifyProjectedResource(input: {
return 'audio';
}
if (
normalizedKind === 'ui' ||
normalizedMediaType.startsWith('image/') ||
normalizedMediaType.startsWith('video/') ||
artExtension.test(normalizedPath) ||
@@ -26,6 +26,54 @@ export function InputSidebar({
return (
<aside className="min-h-0 overflow-y-auto border-r border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-3">
<div className="mb-3 grid gap-2">
<button
type="button"
className="rounded-lg bg-violet-600 px-3 py-2 text-left text-xs font-semibold text-white disabled:cursor-wait disabled:opacity-60"
onClick={() => void controller.mergeUi()}
disabled={
controller.isMerging ||
controller.editor.isLocked ||
controller.editor.state.ui_trees.length === 0
}
title="调试:合并 UI 树"
>
{controller.isMerging ? '界面树合并中…' : '合并 UI 树'}
</button>
<button
type="button"
className="rounded-lg bg-blue-600 px-3 py-2 text-left text-xs font-semibold text-white disabled:cursor-wait disabled:opacity-60"
onClick={() => void controller.recognizeUi()}
disabled={controller.isRecognizing || controller.editor.isLocked}
title="调试:识别 UI 结构"
>
{controller.isRecognizing ? '结构识别中…' : '识别 UI 结构'}
</button>
<button
type="button"
className="rounded-lg bg-orange-500 px-3 py-2 text-left text-xs font-semibold text-white disabled:cursor-wait disabled:opacity-60"
onClick={() => void controller.suggestUiDesignSemantics()}
disabled={controller.isSuggesting || controller.editor.isLocked}
title="调试:识别参考图语义"
>
{controller.isSuggesting ? '识别中…' : '识别参考图语义'}
</button>
{controller.mergeStatus ? (
<p className="m-0 rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) px-3 py-2 text-xs">
{controller.mergeStatus}
</p>
) : null}
{controller.recognitionStatus ? (
<p className="m-0 rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) px-3 py-2 text-xs">
{controller.recognitionStatus}
</p>
) : null}
{controller.suggestionStatus ? (
<p className="m-0 rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) px-3 py-2 text-xs">
{controller.suggestionStatus}
</p>
) : null}
</div>
<section className="rounded-xl border border-(--platform-subpanel-border) bg-white/35 p-3">
<div className="flex items-center justify-between">
<div>
@@ -1,3 +1,11 @@
import { ChevronLeft } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { ThemedModal } from '../../components/modal/ThemedModal';
import {
mockUiDesignStateStore,
type UiDesignStateStore,
} from '../../features/ui-editor/uiDesignStateStore';
import { EditorDialogs } from './components/EditorDialogs';
import { InputSidebar } from './components/InputSidebar';
import { InspectorSidebar } from './components/Inspector/InspectorSidebar';
@@ -7,13 +15,97 @@ import { useUiEditorPage } from './useUiEditorPage';
export default function UiEditorPage({
projectPath = '/tmp/ui_editor',
resourceId,
resourceLabel,
onBack,
stateStore = mockUiDesignStateStore,
}: {
projectPath?: string;
resourceId?: string;
resourceLabel?: string;
onBack?: () => void;
stateStore?: UiDesignStateStore;
}) {
const controller = useUiEditorPage(projectPath);
const controller = useUiEditorPage(projectPath, resourceId, stateStore);
const [savedStateSignature, setSavedStateSignature] = useState<string | null>(
null,
);
const [saveError, setSaveError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [returnConfirmOpen, setReturnConfirmOpen] = useState(false);
const loadedResourceIdRef = useRef<string | undefined>(undefined);
const stateSignature = JSON.stringify(controller.editor.state);
useEffect(() => {
if (!resourceId || controller.isLoading) return;
if (loadedResourceIdRef.current !== resourceId) {
loadedResourceIdRef.current = resourceId;
setSavedStateSignature(stateSignature);
setSaveError(null);
}
}, [controller.isLoading, resourceId, stateSignature]);
const isDirty =
resourceId !== undefined &&
savedStateSignature !== null &&
savedStateSignature !== stateSignature;
async function save() {
if (!resourceId || isSaving) return;
setSaveError(null);
setIsSaving(true);
try {
await stateStore.save(resourceId, controller.editor.state);
setSavedStateSignature(JSON.stringify(controller.editor.state));
} catch (error) {
setSaveError(error instanceof Error ? error.message : String(error));
throw error;
} finally {
setIsSaving(false);
}
}
async function saveAndReturn() {
try {
await save();
setReturnConfirmOpen(false);
onBack?.();
} catch {
// The save error stays visible in the editor.
}
}
function requestBack() {
if (isDirty) {
setReturnConfirmOpen(true);
return;
}
onBack?.();
}
return (
<main className="flex h-[calc(100vh-42px)] min-h-0 min-w-5xl flex-col overflow-hidden bg-(--platform-body-fill) pt-12 text-(--platform-text-strong)">
<main className="flex h-full min-h-0 min-w-5xl flex-col overflow-hidden bg-(--platform-body-fill) text-(--platform-text-strong)">
{resourceId ? (
<header className="flex shrink-0 items-center justify-between border-b border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) px-4 py-2">
<button
type="button"
className="inline-flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm hover:bg-black/4"
onClick={requestBack}
>
<ChevronLeft size={17} aria-hidden="true" />
</button>
<strong className="text-sm">{resourceLabel ?? 'UI 设计'}</strong>
<button
type="button"
className="rounded-lg bg-orange-600 px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-60"
disabled={isSaving || controller.isLoading}
onClick={() => void save()}
>
{isSaving ? '保存中…' : '保存'}
</button>
</header>
) : null}
<ToolNavigation
activeTool={controller.activeTool}
onChange={controller.selectTool}
@@ -24,6 +116,46 @@ export default function UiEditorPage({
<InspectorSidebar controller={controller} />
</div>
<EditorDialogs controller={controller} />
<ThemedModal
open={returnConfirmOpen}
onClose={() => setReturnConfirmOpen(false)}
ariaLabel="确认返回资源"
panelClassName="w-[420px] rounded-2xl p-5"
>
<h2 className="m-0 text-base font-semibold"></h2>
{saveError ? (
<p className="mt-3 text-sm text-red-600" role="alert">
{saveError}
</p>
) : null}
<div className="mt-5 flex justify-end gap-2">
<button
type="button"
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-xs"
onClick={() => setReturnConfirmOpen(false)}
>
</button>
<button
type="button"
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-2 text-xs"
onClick={() => {
setReturnConfirmOpen(false);
onBack?.();
}}
>
</button>
<button
type="button"
className="rounded-lg bg-orange-600 px-3 py-2 text-xs font-semibold text-white disabled:opacity-60"
disabled={isSaving}
onClick={() => void saveAndReturn()}
>
{isSaving ? '保存中…' : '保存并返回'}
</button>
</div>
</ThemedModal>
<div className="fixed bottom-4 right-4 z-30 flex max-w-xs flex-col items-end gap-2">
{controller.suggestionStatus ? (
<p className="rounded-lg border border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) px-3 py-2 text-xs shadow-lg">
@@ -1,18 +1,18 @@
import { invoke } from '@tauri-apps/api/core';
import { useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ImportedAsset } from '../../components/AssetImporter';
import {
prepareDesignImageBatch,
prepareSpriteAssetBatch,
} from '../../features/ui-editor/importAdapter';
import { applyMergeResult } from '../../features/ui-editor/merge';
import {
type UiEditorPrerequisiteIssue,
validateAssetRecognitionPrerequisites,
validateComponentRecognitionPrerequisites,
validateLayoutReviewPrerequisites,
} from '../../features/ui-editor/prerequisites';
import { applyMergeResult } from '../../features/ui-editor/merge';
import { applyRecognitionResult } from '../../features/ui-editor/recognition';
import { applyBindingResult } from '../../features/ui-editor/binding';
import type { BindingDTO } from '../../features/ui-editor/types/BindingDTO';
@@ -26,6 +26,10 @@ import type { SpriteBorder } from '../../features/ui-editor/types/SpriteBorder';
import type { UIDesignImageId } from '../../features/ui-editor/types/UIDesignImageId';
import type { UIDesignImageRole } from '../../features/ui-editor/types/UIDesignImageRole';
import type { UIDesignSuggestionTreeNode } from '../../features/ui-editor/types/UIDesignSuggestionTreeNode';
import {
mockUiDesignStateStore,
type UiDesignStateStore,
} from '../../features/ui-editor/uiDesignStateStore';
import { applyUiDesignSuggestions } from '../../features/ui-editor/uiDesignSuggestions';
import {
EMPTY_UI_EDITOR_STATE,
@@ -43,8 +47,13 @@ import {
const ASSET_BATCH_SIZE = 5;
export function useUiEditorPage(projectPath: string) {
export function useUiEditorPage(
projectPath: string,
resourceId?: string,
stateStore: UiDesignStateStore = mockUiDesignStateStore,
) {
const editor = useUiEditorState(EMPTY_UI_EDITOR_STATE);
const [isLoading, setIsLoading] = useState(Boolean(resourceId));
const [activeTool, setActiveTool] = useState<UiEditorToolId>('input');
const [imageOrder, setImageOrder] = useState<UIDesignImageId[]>([]);
const [activeImageId, setActiveImageId] = useState<UIDesignImageId | null>(
@@ -74,6 +83,35 @@ export function useUiEditorPage(projectPath: string) {
const [isBinding, setIsBinding] = useState(false);
const [bindingStatus, setBindingStatus] = useState<string | null>(null);
useEffect(() => {
if (!resourceId) {
setIsLoading(false);
return;
}
let cancelled = false;
setIsLoading(true);
void stateStore
.load(resourceId)
.then((state) => {
if (!cancelled) {
editor.replaceState(state);
}
})
.catch((cause: unknown) => {
if (!cancelled) {
setStatus(cause instanceof Error ? cause.message : String(cause));
}
})
.finally(() => {
if (!cancelled) {
setIsLoading(false);
}
});
return () => {
cancelled = true;
};
}, [editor.replaceState, resourceId, stateStore]);
const images = editor.state.ui_design_images;
const sprites = editor.state.sprite_assets;
const activeImage = activeImageId ? images[activeImageId] : null;
@@ -111,7 +149,7 @@ export function useUiEditorPage(projectPath: string) {
? editor.state.ui_trees.find((tree) => tree.src_ui_design === activeImageId)
: null;
function findNodeContext(
const findNodeContext = useCallback(function findNodeContext(
node: UiNode,
nodeId: NodeId,
parentSize: { width: number; height: number },
@@ -132,7 +170,7 @@ export function useUiEditorPage(projectPath: string) {
if (found) return found;
}
return null;
}
}, []);
const selectedNodeContext = useMemo(() => {
if (!activeImage || !treeForActiveImage || !selectedNodeId) return null;
@@ -144,7 +182,7 @@ export function useUiEditorPage(projectPath: string) {
width,
height,
});
}, [activeImage, selectedNodeId, treeForActiveImage]);
}, [activeImage, findNodeContext, selectedNodeId, treeForActiveImage]);
async function importAssets(imported: ImportedAsset[]) {
if (!importKind || !projectPath) return;
@@ -550,6 +588,8 @@ export function useUiEditorPage(projectPath: string) {
return {
projectPath,
resourceId,
isLoading,
editor,
activeTool,
activeToolLabel,
@@ -182,6 +182,33 @@ describe('项目资源投影', () => {
expect(first.map(({ id }) => id)).toEqual(second.map(({ id }) => id));
});
it('把 manifest 中的 UI 设计登记为美术资源', () => {
const manifest = createGameCreationAppManifest(
'ui-resource',
'UI 资源测试',
);
manifest.assets = [
{
id: 'ui-1',
kind: 'UI',
mediaType: 'application/json',
localPath: 'ui/UI 设计 1.json',
source: { kind: 'generated', resourceId: 'ui:1' },
},
];
expect(projectResourcesFromReadModels(manifest, [], [])).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: 'asset:ui-1',
category: 'art',
subtype: 'UI',
manifestAssetId: 'ui-1',
}),
]),
);
});
it('只从 manifest 投影版本并保留直接父子关系', () => {
const manifest = createGameCreationAppManifest(
'version-projection',
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { mockUiDesignStateStore } from '../src/features/ui-editor/uiDesignStateStore';
describe('UI 设计 State mock store', () => {
it('每次 load 返回一个新的空 Statesave 仍按成功完成', async () => {
const first = await mockUiDesignStateStore.load('ui-1');
first.ui_trees.push({} as never);
await expect(
mockUiDesignStateStore.save('ui-1', first),
).resolves.toBeUndefined();
await expect(mockUiDesignStateStore.load('ui-1')).resolves.toEqual({
ui_trees: [],
ui_design_images: {},
sprite_assets: {},
font_assets: {},
});
});
});