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); }, ); });