重写 UI 设计文档桥接单测到当前入口

- 退役 ensureUiDesignResourceForPrototype / findLinkedUiDesignResource 的 6 条旧用例已随实现删除而无对应导出,测试文件整体失败
- 改为覆盖 createUiDesignDocFromImages:空图片列表拒绝且不调用命令,正常调用透传项目身份与设计图引用
This commit is contained in:
2026-09-23 21:35:57 +08:00
parent e360d12ccb
commit 544f9ac040
@@ -1,201 +1,43 @@
import { describe, expect, it, vi } from 'vitest';
import type {
GameCreationAppAssetManifestEntry,
GameCreationAppManifest,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
ensureUiDesignResourceForPrototype,
findLinkedUiDesignResource,
} from '../src/features/ui-editor/uiDesignResourceBridge';
function manifestWithPrototype(): GameCreationAppManifest {
const prototype: GameCreationAppAssetManifestEntry = {
id: 'prototype-asset',
kind: 'ui-design',
mediaType: 'image/png',
localPath: 'assets/ui-prototype.png',
source: { kind: 'canvas', referenceResourceIds: [] },
imageSequenceFrames: null,
imageSequenceDurationMs: null,
};
return {
schemaVersion: 'game-creator-app.v1',
projectId: 'project-1',
projectName: 'UI bridge',
tasks: [],
assets: [prototype],
versions: [],
godotProjectRoot: null,
preview: null,
resourceCanvasLayouts: [],
} as unknown as GameCreationAppManifest;
}
import { createUiDesignDocFromImages } from '../src/features/ui-editor/uiDesignResourceBridge';
describe('ui design resource bridge', () => {
it('finds only a JSON UI resource linked to the exact prototype asset', () => {
const manifest = manifestWithPrototype();
manifest.assets.push({
id: 'unrelated-ui',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/unrelated.json',
source: { kind: 'generated', referenceResourceIds: ['other'] },
imageSequenceFrames: null,
imageSequenceDurationMs: null,
});
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')).toBeNull();
manifest.assets.push({
id: 'linked-ui',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/linked.json',
source: { kind: 'generated', referenceResourceIds: ['prototype-asset'] },
imageSequenceFrames: null,
imageSequenceDurationMs: null,
});
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe(
'linked-ui',
);
});
it('reuses workflow resources linked by the prototype canonical resource id', () => {
const manifest = manifestWithPrototype();
manifest.assets[0]!.source.resourceId = 'canvas-resource-1';
manifest.assets.push({
id: 'workflow-ui',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/workflow.json',
source: {
kind: 'generated',
resourceId: 'ui-workflow-page-1',
referenceResourceIds: ['canvas-resource-1', 'design-resource-1'],
},
imageSequenceFrames: null,
imageSequenceDurationMs: null,
});
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe(
'workflow-ui',
);
});
it('prefers a completed workflow resource when an older bridge resource also matches', () => {
const manifest = manifestWithPrototype();
manifest.assets.push(
{
id: 'bridge-ui',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/UI 设计 1.json',
source: {
kind: 'generated',
referenceResourceIds: ['prototype-asset'],
},
imageSequenceFrames: null,
imageSequenceDurationMs: null,
},
{
id: 'completed-ui',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/ui-workflow-page.json',
source: {
kind: 'generated',
generationKind: 'ui-workflow.completed',
referenceResourceIds: ['prototype-asset'],
},
imageSequenceFrames: null,
imageSequenceDurationMs: null,
},
);
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')?.id).toBe(
'completed-ui',
);
});
it('reuses an existing link without invoking a mutating command', async () => {
const manifest = manifestWithPrototype();
const linked = {
id: 'linked-ui',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/linked.json',
source: {
kind: 'generated' as const,
referenceResourceIds: ['prototype-asset'],
},
imageSequenceFrames: null,
imageSequenceDurationMs: null,
};
manifest.assets.push(linked);
it('rejects an empty image list without invoking the command', async () => {
const invoke = vi.fn();
const result = await ensureUiDesignResourceForPrototype({
await expect(
createUiDesignDocFromImages({
projectPath: '/project',
expectedProjectId: 'project-1',
images: [],
invoke,
}),
).rejects.toThrow('请至少选择一张界面图');
expect(invoke).not.toHaveBeenCalled();
});
it('creates a document through the command with the project identity injected', async () => {
const created = {
asset: { id: 'ui-1' },
manifest: { projectId: 'project-1' },
relativePath: 'ui/UI 设计 1.json',
imageIds: ['image-a'],
committedProjectRevision: 7,
};
const invoke = vi.fn().mockResolvedValue(created);
const result = await createUiDesignDocFromImages({
projectPath: '/project',
manifest,
prototypeAssetId: 'prototype-asset',
expectedProjectId: 'project-1',
images: [{ assetId: 'image-a', path: null }],
invoke,
});
expect(result.asset).toEqual(linked);
expect(result.created).toBe(false);
expect(invoke).not.toHaveBeenCalled();
});
it('invokes the atomic Tauri bridge for a missing link', async () => {
const manifest = manifestWithPrototype();
const result = {
asset: {
id: 'new-ui',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/UI 设计 1.json',
source: {
kind: 'generated' as const,
referenceResourceIds: ['prototype-asset'],
},
imageSequenceFrames: null,
imageSequenceDurationMs: null,
},
manifest: { ...manifest, assets: [...manifest.assets] },
committedProjectRevision: 7,
created: true,
};
const invoke = vi.fn().mockResolvedValue(result);
await expect(
ensureUiDesignResourceForPrototype({
expect(invoke).toHaveBeenCalledWith('create_ui_design_doc_from_images', {
input: {
projectPath: '/project',
manifest,
prototypeAssetId: ' prototype-asset ',
invoke,
}),
).resolves.toEqual(result);
expect(invoke).toHaveBeenCalledWith(
'ensure_ui_design_resource_for_prototype',
{
input: {
projectPath: '/project',
expectedProjectId: 'project-1',
prototypeAssetId: 'prototype-asset',
},
expectedProjectId: 'project-1',
images: [{ assetId: 'image-a', path: null }],
},
);
});
it('rejects non-prototype assets before invoking Tauri', async () => {
const manifest = manifestWithPrototype();
const invoke = vi.fn();
await expect(
ensureUiDesignResourceForPrototype({
projectPath: '/project',
manifest,
prototypeAssetId: 'missing',
invoke,
}),
).rejects.toThrow('目标资源不是 UI 原型图片');
expect(invoke).not.toHaveBeenCalled();
});
expect(result).toBe(created);
});
});