910862fd06
稳定工作台与窗口标题栏状态同步,消除重复更新循环 支持画布右键平移并保留左键框选及资源拖动,完善中断清理 解耦运行不可用提示与资源选中状态 复用原生UI状态校验区分UI设计JSON和普通JSON,接入现有编辑器与代码预览 补充前端和原生回归测试、规范及待验收记录,明确对话历史分页尚未修复
262 lines
9.2 KiB
TypeScript
262 lines
9.2 KiB
TypeScript
// @vitest-environment jsdom
|
||
// 真机取证:用真机 `What do u wanna do kitten` manifest 的逐条真实形状,
|
||
// 可执行地数出「每张卡走哪个预览分支」「最终落哪个画布栏目」「实际发出哪些 IPC」。
|
||
//
|
||
// 真机数据(只读读出,未修改):
|
||
// assets 61 条 = ui/image/png ×49、ui/application/json ×8、art-spritesheet/image/png ×1、
|
||
// game-background/image/png ×1、game-entry/text/html ×1、icon-spec/image/png ×1
|
||
// 每条都带显式 category:scene ×1、ui-interaction ×2、unclassified ×58
|
||
// versions 5 条
|
||
import { describe, expect, test } from 'vitest';
|
||
|
||
import {
|
||
gameCreationAppAssetCategory,
|
||
type GameCreationAppAssetManifestEntry,
|
||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import { projectResourceCardPreviewKind } from '../src/view/project-development/resourceCardPreviewModel';
|
||
import {
|
||
projectResourceCanvasCategory,
|
||
projectResourcesFromReadModels,
|
||
projectResourceTypeLabel,
|
||
} from '../src/view/project-development/resourceProjectionModel';
|
||
|
||
type Shape = {
|
||
kind: string;
|
||
mediaType: string;
|
||
ext: string;
|
||
count: number;
|
||
category: 'scene' | 'ui-interaction' | 'unclassified';
|
||
};
|
||
|
||
/** 真机 manifest 的 assets 逐条形状(category 为磁盘上的持久化值)。 */
|
||
const REAL_ASSET_SHAPES: readonly Shape[] = [
|
||
{
|
||
kind: 'ui',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 49,
|
||
category: 'unclassified',
|
||
},
|
||
{
|
||
kind: 'ui',
|
||
mediaType: 'application/json',
|
||
ext: 'json',
|
||
count: 8,
|
||
category: 'unclassified',
|
||
},
|
||
{
|
||
kind: 'art-spritesheet',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 1,
|
||
category: 'ui-interaction',
|
||
},
|
||
{
|
||
kind: 'game-background',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 1,
|
||
category: 'scene',
|
||
},
|
||
{
|
||
kind: 'game-entry',
|
||
mediaType: 'text/html',
|
||
ext: 'html',
|
||
count: 1,
|
||
category: 'unclassified',
|
||
},
|
||
{
|
||
kind: 'icon-spec',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 1,
|
||
category: 'ui-interaction',
|
||
},
|
||
];
|
||
|
||
function buildRealAssets(): GameCreationAppAssetManifestEntry[] {
|
||
const assets: GameCreationAppAssetManifestEntry[] = [];
|
||
let index = 0;
|
||
for (const shape of REAL_ASSET_SHAPES) {
|
||
for (let i = 0; i < shape.count; i += 1) {
|
||
index += 1;
|
||
assets.push({
|
||
id: `asset-${index}`,
|
||
kind: shape.kind,
|
||
mediaType: shape.mediaType,
|
||
localPath: `assets/probe-${index}.${shape.ext}`,
|
||
source: { kind: 'generated' },
|
||
category: shape.category,
|
||
});
|
||
}
|
||
}
|
||
return assets;
|
||
}
|
||
|
||
function realManifest() {
|
||
return {
|
||
schemaVersion: 'game-creation-app.v1',
|
||
projectId: 'agc-real-probe',
|
||
name: '真机取证项目',
|
||
tasks: [],
|
||
assets: buildRealAssets(),
|
||
};
|
||
}
|
||
|
||
function countBy<T>(items: readonly T[], key: (item: T) => string) {
|
||
const counts = new Map<string, number>();
|
||
for (const item of items) {
|
||
const value = key(item);
|
||
counts.set(value, (counts.get(value) ?? 0) + 1);
|
||
}
|
||
return Object.fromEntries(counts);
|
||
}
|
||
|
||
describe('真机 manifest 取证:占位卡片计数与栏目分布', () => {
|
||
test('61 条真机资产全部进入资源投影', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
expect(resources).toHaveLength(61);
|
||
});
|
||
|
||
test('存量 57 条 ui 的落盘 unclassified 被读时重派生自愈', () => {
|
||
const assets = realManifest().assets;
|
||
const uiAssets = assets.filter((asset) => asset.kind === 'ui');
|
||
|
||
// 前置事实:这 57 条落盘值确实是 unclassified(历史误判现场)。
|
||
expect(uiAssets).toHaveLength(57);
|
||
expect(uiAssets.every((asset) => asset.category === 'unclassified')).toBe(
|
||
true,
|
||
);
|
||
// 读取侧已经把它重派生成 ui-interaction:读时自愈,不回写 manifest。
|
||
expect(
|
||
uiAssets.every(
|
||
(asset) => gameCreationAppAssetCategory(asset) === 'ui-interaction',
|
||
),
|
||
).toBe(true);
|
||
|
||
// 直接钉住「读时重派生」这一行为本身:落盘 unclassified + kind 能派生明确分类
|
||
// → 用派生值;这正是修复前那 57 条留在「待归类」的原因。
|
||
expect(
|
||
gameCreationAppAssetCategory({ kind: 'ui', category: 'unclassified' }),
|
||
).toBe('ui-interaction');
|
||
// 落盘值本身就是明确分类时仍然信任落盘值(用户手动设置的权威性)。
|
||
expect(
|
||
gameCreationAppAssetCategory({ kind: 'ui', category: 'scene' }),
|
||
).toBe('scene');
|
||
// 落盘值缺失时按 kind 派生(历史 manifest 兼容)。
|
||
expect(
|
||
gameCreationAppAssetCategory({ kind: 'ui', category: undefined }),
|
||
).toBe('ui-interaction');
|
||
});
|
||
|
||
test('落盘 unclassified 且 kind 派生也是 unclassified 时不受影响', () => {
|
||
// image / video / code 的 canonical 分类本来就是 unclassified,
|
||
// 这类资产必须继续信任落盘值,不被重派生规则误伤。
|
||
for (const kind of ['image', 'video', 'code', 'publication-material']) {
|
||
expect(
|
||
gameCreationAppAssetCategory({ kind, category: 'unclassified' }),
|
||
).toBe('unclassified');
|
||
}
|
||
});
|
||
|
||
test('读时重派生后:57 条 ui 归位 UI 交互,待归类只剩 code 类 game-entry', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const byCategory = countBy(resources, (resource) =>
|
||
projectResourceCanvasCategory(resource),
|
||
);
|
||
|
||
// 存量自愈后的真实分布:57 条 ui 归位到 UI 交互,只剩 game-entry(code)在待归类。
|
||
expect(byCategory).toEqual({
|
||
unclassified: 1,
|
||
'ui-interaction': 59,
|
||
scene: 1,
|
||
});
|
||
const uiResources = resources.filter(
|
||
(resource) => resource.subtype === 'ui',
|
||
);
|
||
expect(uiResources).toHaveLength(57);
|
||
expect(
|
||
uiResources.every(
|
||
(resource) =>
|
||
projectResourceCanvasCategory(resource) === 'ui-interaction',
|
||
),
|
||
).toBe(true);
|
||
// 唯一留在待归类的就是 game-entry:code → unclassified 是分类表的设计口径。
|
||
expect(
|
||
resources
|
||
.filter(
|
||
(resource) =>
|
||
projectResourceCanvasCategory(resource) === 'unclassified',
|
||
)
|
||
.map((resource) => resource.subtype),
|
||
).toEqual(['game-entry']);
|
||
});
|
||
|
||
test('按预览分支计数:52 张 PNG 走图片分支,8 条 UI 规格走文档分支,无一落占位', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const previewKindByResource = resources.map((resource) => ({
|
||
resource,
|
||
previewKind: projectResourceCardPreviewKind(resource),
|
||
typeLabel: projectResourceTypeLabel(resource),
|
||
canvasCategory: projectResourceCanvasCategory(resource),
|
||
}));
|
||
|
||
const byPreviewKind = countBy(previewKindByResource, (e) => e.previewKind);
|
||
// 这里曾经是 `placeholder: 8`:8 条 `ui/application/json` 因为 `kind:"UI"` 命中
|
||
// `artKind`(其中含 `ui`)而被判成 art,于是角标显示「图片」,而预览调度按 art 走
|
||
// 图像分支、又因 mediaType 不是图像而兜底成 placeholder —— 卡片永不发起读取,
|
||
// 表现为「标着图片却只有占位图标」。类型判定改为 mediaType/扩展名优先、kind 只做
|
||
// 兜底后,它们进入文本读取通道;卡面再消费原生识别结果显示 UI 设计或 JSON。
|
||
expect(byPreviewKind).toEqual({
|
||
'raster-image': 52,
|
||
document: 8,
|
||
code: 1,
|
||
});
|
||
// 占位分支在真机上必须为空 —— 上面那条 `toEqual` 已经钉住了唯一取值集合,
|
||
// 这里不再另加"不存在"式断言,避免写成恒真守卫。
|
||
const documentAssets = previewKindByResource.filter(
|
||
(e) => e.previewKind === 'document',
|
||
);
|
||
expect(countBy(documentAssets, (e) => e.resource.mediaType)).toEqual({
|
||
'application/json': 8,
|
||
});
|
||
expect(countBy(documentAssets, (e) => e.resource.subtype)).toEqual({
|
||
ui: 8,
|
||
});
|
||
// 角标与预览分支必须是同一套口径:标「图片」的必须真的走图片分支。
|
||
for (const entry of previewKindByResource) {
|
||
if (entry.typeLabel === '图片') {
|
||
expect(entry.previewKind).toBe('raster-image');
|
||
}
|
||
if (entry.typeLabel === '文档') {
|
||
expect(entry.previewKind).toBe('document');
|
||
}
|
||
}
|
||
});
|
||
|
||
test('按画布栏目计数:待归类从 58 降到 1,UI 交互升到 59', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const byCategory = countBy(resources, (resource) =>
|
||
projectResourceCanvasCategory(resource),
|
||
);
|
||
|
||
expect(byCategory).toEqual({
|
||
unclassified: 1,
|
||
'ui-interaction': 59,
|
||
scene: 1,
|
||
});
|
||
});
|
||
|
||
test('预览分支与画布栏目是两个独立轴:落 UI 交互的 PNG 仍走图片分支', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const iconSpec = resources.find(
|
||
(resource) => resource.subtype === 'icon-spec',
|
||
);
|
||
expect(iconSpec).toBeDefined();
|
||
// 真机 icon-spec 落在 UI 交互栏目。
|
||
expect(projectResourceCanvasCategory(iconSpec!)).toBe('ui-interaction');
|
||
// 但它仍必须走 PNG 的图片预览分支,而不是媒体分支。
|
||
expect(projectResourceCardPreviewKind(iconSpec!)).toBe('raster-image');
|
||
});
|
||
});
|