import { describe, expect, it } from 'vitest'; import { oneStep } from '../../src/core/commands/CompoundCommand'; import { GroupObjectsCommand } from '../../src/core/commands/groupCommands'; import { findObject, groupMemberIds, groupRowId, hierarchyRows, type HierarchyRow, } from '../../src/core/model/hierarchy'; import { hierarchyDrop } from '../../src/editor/panels/hierarchyDrop'; import type { DropSpot } from '../../src/ui/widgets/ListView'; import { makeSpriteScene, place } from '../helpers'; /** Top-level a, b, c, d, e; b and d form the group "Crates", so the list reads a [b d] c e. */ function grouped() { const f = makeSpriteScene(); const [a, b, c, d, e] = [0, 1, 2, 3, 4].map((i) => place(f, f.spriteId, { x: i * 10, y: 0 })); const k = place(f, f.spriteId, { x: 0, y: 10 }, a); const group = new GroupObjectsCommand(f.sceneId, [b!, d!], 'Crates'); f.stack.execute(group); const row = groupRowId(group.groupId); /** Drops like the panel does, with the given rows unfolded; returns how many commands it made. */ const drop = (dragged: string, spot: DropSpot, open: string[] = [row]): number => { const { commands } = hierarchyDrop(f.sceneOf(), dragged, spot, (id) => open.includes(id)); if (commands.length > 0) f.stack.execute(oneStep('Move object', commands)); return commands.length; }; const label = (r: HierarchyRow) => (r.kind === 'group' ? `[${r.group.name}]` : name(r.object.id)); const names: Record = { [a!]: 'a', [b!]: 'b', [c!]: 'c', [d!]: 'd', [e!]: 'e', [k]: 'k' }; const name = (id: string) => names[id] ?? '?'; /** The open list as text: a group's members follow it one step in. */ const listed = () => hierarchyRows(f.sceneOf(), new Set([row, a!])) .map((r) => `${'.'.repeat(r.depth)}${label(r)}`) .join(' '); const members = () => groupMemberIds(f.sceneOf(), group.groupId).map(name); return { ...f, a: a!, b: b!, c: c!, d: d!, e: e!, k, row, drop, listed, members }; } describe('dropping rows in the hierarchy with groups (#241)', () => { it('starts from a group listed as one block', () => { const f = grouped(); expect(f.listed()).toBe('a .k [Crates] .b .d c e'); }); it('puts an object dropped onto the group row behind its last member', () => { const f = grouped(); f.drop(f.e, { at: 'onto', targetId: f.row }); expect(f.members()).toEqual(['b', 'd', 'e']); expect(f.listed()).toBe('a .k [Crates] .b .d .e c'); f.stack.undo(); expect(f.listed()).toBe('a .k [Crates] .b .d c e'); }); it('joins the group between two members, and at the top just below an open group row', () => { const f = grouped(); f.drop(f.e, { at: 'after', targetId: f.b }); expect(f.listed()).toBe('a .k [Crates] .b .e .d c'); f.drop(f.c, { at: 'after', targetId: f.row }); expect(f.listed()).toBe('a .k [Crates] .c .b .e .d'); }); it('goes behind a folded group, and in front of any group row, without joining', () => { const f = grouped(); f.drop(f.a, { at: 'after', targetId: f.row }, []); expect(f.listed()).toBe('[Crates] .b .d a .k c e'); f.drop(f.e, { at: 'before', targetId: f.row }); expect(f.listed()).toBe('e [Crates] .b .d a .k c'); expect(f.members()).toEqual(['b', 'd']); }); it('takes a member out when it is dropped beside a row outside the group', () => { const f = grouped(); f.drop(f.d, { at: 'after', targetId: f.e }); expect(f.members()).toEqual(['b']); expect(f.listed()).toBe('a .k [Crates] .b c e d'); }); it('brings a child up to the top level when it is dropped into a group', () => { const f = grouped(); f.drop(f.k, { at: 'before', targetId: f.d }); expect(findObject(f.sceneOf(), f.k)!.parentId).toBe(''); expect(f.listed()).toBe('a [Crates] .b .k .d c e'); }); it('takes a member out of the group when it is dropped onto another object', () => { const f = grouped(); f.drop(f.b, { at: 'onto', targetId: f.c }); expect(findObject(f.sceneOf(), f.b)!.groupId).toBeUndefined(); expect(f.members()).toEqual(['d']); }); it('moves a group row with all its members as one block', () => { const f = grouped(); f.drop(f.row, { at: 'after', targetId: f.e }); expect(f.listed()).toBe('a .k c e [Crates] .b .d'); f.drop(f.row, { at: 'before', targetId: f.a }); expect(f.listed()).toBe('[Crates] .b .d a .k c e'); // One undo takes the whole block back. f.stack.undo(); expect(f.listed()).toBe('a .k c e [Crates] .b .d'); }); it('makes nothing of a drop that leaves everything where it is', () => { const f = grouped(); expect(f.drop(f.d, { at: 'after', targetId: f.b })).toBe(0); expect(f.drop(f.b, { at: 'onto', targetId: f.row })).toBe(0); expect(f.drop(f.row, { at: 'before', targetId: f.c })).toBe(0); expect(f.drop(f.row, { at: 'onto', targetId: f.c })).toBe(0); expect(f.drop(f.row, { at: 'before', targetId: f.k })).toBe(0); expect(f.listed()).toBe('a .k [Crates] .b .d c e'); }); });