Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts
T
menghao 216407d93e
Project CI / Frontend tests (push) Failing after 20s
Project CI / Repository checks (push) Successful in 1m2s
Project CI / Backend tests (push) Successful in 2m59s
Project CI / Native shell tests (push) Successful in 11m58s
实现资源画布布局持久化 (#116)
冻结资源画布布局数据与 CAS 合同
实现双模式本地 sidecar 安全读写
接入二维拖动、默认排版和跨重启恢复
补齐并发冲突、安全边界和界面测试
同步技术文档与共享决策

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/116
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: menghao <mh18530625731@163.com>
Co-committed-by: menghao <mh18530625731@163.com>
2026-07-31 12:00:48 +08:00

209 lines
5.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
createEmptyResourceCanvasLayout,
moveResourceCanvasPosition,
reconcileResourceCanvasLayout,
RESOURCE_CANVAS_CARD_HEIGHT,
RESOURCE_CANVAS_CARD_WIDTH,
RESOURCE_CANVAS_COLUMN_GAP,
RESOURCE_CANVAS_ROW_GAP,
type ResourceCanvasItem,
} from '../src/view/project-development/resourceCanvasLayoutModel';
function resource(
id: string,
category: ResourceCanvasItem['category'],
dependencyDepth = 0,
): ResourceCanvasItem {
return {
id,
category,
subtype: 'default',
dependencyDepth,
label: id,
mediaType: category === 'art' ? 'image/png' : 'text/markdown',
};
}
describe('resource canvas layout model', () => {
it('creates non-overlapping defaults and keeps the two modes independent', () => {
const resources = [
resource('a', 'art', 0),
resource('b', 'art', 0),
resource('c', 'art', 1),
];
const dependency = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-1', 'dependency'),
resources,
).layout;
const type = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-1', 'type'),
resources,
).layout;
expect(dependency.positions).toMatchObject([
{ resourceId: 'a', x: 0, y: 0, manuallyPlaced: false },
{
resourceId: 'c',
x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP,
y: 0,
manuallyPlaced: false,
},
{
resourceId: 'b',
x: 0,
y: RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_ROW_GAP,
manuallyPlaced: false,
},
]);
expect(type.positions).toMatchObject([
{ resourceId: 'a', x: 0, y: 0 },
{
resourceId: 'b',
x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP,
y: 0,
},
{
resourceId: 'c',
x: 2 * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP),
y: 0,
},
]);
});
it('preserves existing positions, appends new resources, and removes stale ids', () => {
const base = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-1', 'type'),
[resource('a', 'document'), resource('stale', 'document')],
).layout;
const moved = moveResourceCanvasPosition(
base,
'a',
'document',
333.4,
41.6,
);
const reconciled = reconcileResourceCanvasLayout(moved, [
resource('a', 'document'),
resource('new', 'document'),
]);
expect(reconciled.changed).toBe(true);
expect(reconciled.layout.positions).toContainEqual({
resourceId: 'a',
section: 'document',
x: 333,
y: 42,
manuallyPlaced: true,
});
expect(
reconciled.layout.positions.some(
(position) => position.resourceId === 'stale',
),
).toBe(false);
expect(
reconciled.layout.positions.find(
(position) => position.resourceId === 'new',
)?.manuallyPlaced,
).toBe(false);
});
it('drops a persisted position whose section no longer matches the resource', () => {
const layout = createEmptyResourceCanvasLayout('project-1', 'dependency');
layout.positions = [
{
resourceId: 'asset-a',
section: 'audio',
x: 10,
y: 10,
manuallyPlaced: true,
},
];
const reconciled = reconcileResourceCanvasLayout(layout, [
resource('asset-a', 'art'),
]);
expect(reconciled.layout.positions).toEqual([
{
resourceId: 'asset-a',
section: 'art',
x: 0,
y: 0,
manuallyPlaced: false,
},
]);
});
it('sorts type defaults by subtype before media type and label with an id fallback', () => {
const typeLayout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-type-order', 'type'),
[
{
...resource('ui-prototype', 'art'),
subtype: 'ui-prototype',
mediaType: 'image/png',
label: '甲界面',
},
{
...resource('art-spritesheet-b', 'art'),
subtype: 'art-spritesheet',
mediaType: 'image/png',
label: '乙图集',
},
{
...resource('art-spritesheet-a', 'art'),
subtype: 'art-spritesheet',
mediaType: 'image/png',
label: '乙图集',
},
],
).layout;
expect(
typeLayout.positions.map(({ resourceId, x, y }) => ({
resourceId,
x,
y,
})),
).toEqual([
{ resourceId: 'art-spritesheet-a', x: 0, y: 0 },
{
resourceId: 'art-spritesheet-b',
x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP,
y: 0,
},
{
resourceId: 'ui-prototype',
x: 2 * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP),
y: 0,
},
]);
});
it.each(['dependency', 'type'] as const)(
'reconciles 4096 resources in %s mode within the bounded layout budget',
(mode) => {
const resources = Array.from({ length: 4096 }, (_, index) => ({
...resource(`resource-${index.toString().padStart(4, '0')}`, 'art'),
dependencyDepth: index % 64,
mediaType: `image/type-${index % 16}`,
}));
const startedAt = performance.now();
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-performance', mode),
resources,
).layout;
const elapsedMs = performance.now() - startedAt;
expect(layout.positions).toHaveLength(4096);
expect(
new Set(
layout.positions.map((position) => `${position.x}:${position.y}`),
).size,
).toBe(4096);
expect(elapsedMs).toBeLessThan(2000);
},
);
});