import { same } from '../lib/harness.mjs'; /** * Groups in the hierarchy (#241), made, opened, filled, renamed and dissolved with the real menus, * clicks, keys and a real drag. The fixture's top level is o1, o2, o3, o5; the order rules and * drops are unit-tested, this checks that the panel and the dialog do what those rules say. * Everything is taken back by undo, so the fixture leaves as it came. */ export async function hierarchyGroups(t) { const { check, json, evaluate, click, clickText, drag, press, typeText, select, showTab, undo, shot } = t; const rowIds = () => json('__gm.hierarchyOrder()'); const scene = () => json('__gm.state.activeScene'); const selected = async () => (await json('__gm.state.selection'))?.objectIds ?? []; const rowAt = (id) => json(`__gm.hierarchyRow(${JSON.stringify(id)})`); const menuRow = async (label) => { const rows = (await json('__gm.menuRows()')) ?? []; return { rows, enabled: ((await json('__gm.menuEnabled()')) ?? [])[rows.indexOf(label)] }; }; await press('Escape'); await showTab('scene'); await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()'); // 52a. A child cannot go into a group: the entry is there, greyed out. await select({ kind: 'object', holderId: 'scene1', objectId: 'oc2', objectIds: ['oc2'] }); const child = await rowAt('oc2'); await click(child.x, child.y, 'right'); const childEntry = await menuRow('Group…'); check('Group… is greyed out for a child', childEntry.enabled === false, JSON.stringify(childEntry)); await press('Escape'); await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()'); // 52b. Two top-level objects, the row menu's "Group 2 objects…", a name typed into the dialog. await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o3'] }); const first = await rowAt('o1'); await click(first.x, first.y, 'right'); const entry = await menuRow('Group 2 objects…'); await clickText('Group 2 objects…'); const dialogOpened = await json('__gm.groupDialogOpen()'); await typeText('Crates'); await press('Enter'); const made = await scene(); const group = made.groups?.[0]; const row = `group:${group?.id}`; check( 'Group 2 objects… asks for a name and makes the group of both', entry.enabled === true && dialogOpened === true && (await json('__gm.groupDialogOpen()')) === false && made.groups?.length === 1 && group.name === 'Crates' && made.objects .filter((o) => o.groupId === group.id) .map((o) => o.id) .join() === 'o1,o3', JSON.stringify({ entry, dialogOpened, groups: made.groups }), ); // o3 is listed with o1 now; with both members selected the group stays folded. check( 'the group row stands where o1 stood, folded', same(await rowIds(), [row, 'o2', 'o5']), JSON.stringify(await rowIds()), ); // 52c. A click on the group row selects the members, the chevron unfolds them. await evaluate('__gm.state.select(null)'); const groupRow = await rowAt(row); await click(groupRow.x, groupRow.y); check( 'clicking the group row selects all its members and leaves it folded', same(await selected(), ['o1', 'o3']) && same(await rowIds(), [row, 'o2', 'o5']), JSON.stringify({ selected: await selected(), rows: await rowIds() }), ); const chevron = await json(`__gm.hierarchyChevron(${JSON.stringify(row)})`); await click(chevron.x, chevron.y); check( 'its chevron lists the members below it', same(await rowIds(), [row, 'o1', 'o3', 'o2', 'o5']), JSON.stringify(await rowIds()), ); await shot('52-group-open'); // 52d. Dragging o5 onto the group row puts it in behind the last member; undo takes it out. const from = await rowAt('o5'); const onto = await rowAt(row); await drag(from.x, from.y, onto.x, onto.y); check( 'dragging an object onto the group row adds it to the group', same(await rowIds(), [row, 'o1', 'o3', 'o5', 'o2']) && (await scene()).objects.find((o) => o.id === 'o5')?.groupId === group?.id, JSON.stringify(await rowIds()), ); await undo(); // 52e. Rename… from the group row's menu, typed over the selected old name. await click(groupRow.x, groupRow.y, 'right'); const groupMenu = (await json('__gm.menuRows()')) ?? []; await clickText('Rename…'); await typeText('Boxes'); await press('Enter'); check( "the group row's menu renames it", ['Select members', 'Rename…', 'Ungroup'].every((label) => groupMenu.includes(label)) && (await scene()).groups?.[0]?.name === 'Boxes', JSON.stringify({ groupMenu, groups: (await scene()).groups }), ); // 52f. Ungroup keeps the objects and gives back the list of before. await click(groupRow.x, groupRow.y, 'right'); await clickText('Ungroup'); const ungrouped = await scene(); check( 'Ungroup dissolves the group and keeps its members', ungrouped.groups === undefined && !ungrouped.objects.some((o) => o.groupId) && same(await rowIds(), ['o1', 'o2', 'o3', 'o5']), JSON.stringify({ groups: ungrouped.groups, rows: await rowIds() }), ); // Ungroup, rename and group: three steps back to the fixture. await undo(); await undo(); await undo(); const back = await scene(); check( 'undo takes each step back on its own, down to no group at all', back.groups === undefined && !back.objects.some((o) => 'groupId' in o), JSON.stringify(back.groups), ); await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()'); // 52g. A plain click on a row that is already part of a multi-selection keeps only that row – // the other member as well as the primary one. await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2', 'o3'] }); const other = await rowAt('o2'); await click(other.x, other.y); const narrowedToOther = await selected(); await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2', 'o3'] }); const primary = await rowAt('o1'); await click(primary.x, primary.y); const narrowedToPrimary = await selected(); check( 'clicking a selected row narrows the selection to that row', same(narrowedToOther, ['o2']) && same(narrowedToPrimary, ['o1']), JSON.stringify({ narrowedToOther, narrowedToPrimary }), ); await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()'); }