216407d93e
冻结资源画布布局数据与 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>
209 lines
5.7 KiB
TypeScript
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);
|
|
},
|
|
);
|
|
});
|