Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts
T
menghao 4330ec4b5f 修复资源画布前端竞态与布局性能
以 scope epoch 和单写者 FIFO 串行首读、拖动及资源协调写入
CAS 冲突载入权威布局并丢弃旧手动意图,资源协调有界重试
使用空间占用索引将 4096 项默认布局降至可接受复杂度
补充异步竞态、冲突、性能回归测试并同步契约文档
2026-07-30 15:35:01 +08:00

162 lines
4.4 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,
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.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);
},
);
});