import { same } from '../lib/harness.mjs'; /** * Hierarchy folding. A freshly opened scene is fully collapsed, so only its top-level objects * are listed (o4/oc1 hang under o1, oc2 under o2). The chevron folds a row open and shut * without touching the selection, and selecting a nested object unfolds the way down to it. * A click on the selected row pings the object's asset, a double-click opens it (#353). */ export async function hierarchy(t) { const { check, evaluate, json, click, clickText, doubleClick, press, select, sleep, state, shot } = t; const rowIds = () => json('__gm.hierarchyOrder()'); const rows = async () => JSON.stringify(await rowIds()); const selectionBefore = (await state()).selection; check( 'hierarchy starts collapsed – only top-level objects are listed', same(await rowIds(), ['o1', 'o2', 'o3', 'o5']), await rows(), ); check('a leaf row offers no chevron', (await evaluate(`__gm.hierarchyChevron('o3')`)) === null); const chevron = await json(`__gm.hierarchyChevron('o2')`); await click(chevron.x, chevron.y); check( 'clicking the chevron unfolds the row, leaving the selection alone', same(await rowIds(), ['o1', 'o2', 'oc2', 'o3', 'o5']) && same((await state()).selection, selectionBefore), await rows(), ); await shot('01a-hierarchy-unfolded'); await click(chevron.x, chevron.y); check( 'clicking it again folds the subtree away', same(await rowIds(), ['o1', 'o2', 'o3', 'o5']), await rows(), ); // 01b (#353). A click on the row that is selected already pings the object's asset in the asset // panel, as one on the asset link in the inspector does; the first click only selects. const assetOf = (id) => json(`__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(id)}).assetId`); const o3Asset = await assetOf('o3'); await evaluate('__gm.state.pingedAssetId = null'); const o3Row = await json(`__gm.hierarchyRow('o3')`); await click(o3Row.x, o3Row.y); const afterFirst = await json('__gm.state.pingedAssetId'); // Past the double-click window, which goes by time: two quick clicks would open the asset. await sleep(500); await click(o3Row.x, o3Row.y); const ping = await json('__gm.assetPing()'); const pingSelection = (await state()).selection; check( 'a click on the selected row pings its asset and keeps the selection, the first click only selects', afterFirst === null && ping?.id === o3Asset && pingSelection?.kind === 'object' && same(pingSelection.objectIds, ['o3']), JSON.stringify({ afterFirst, ping, o3Asset, pingSelection }), ); // A double-click opens the asset, selected or not, and leaves the row folded as it was. const o2Asset = await assetOf('o2'); const row = await json(`__gm.hierarchyRow('o2')`); await doubleClick(row.x, row.y); const o2Selection = (await state()).selection; check( 'double-clicking a row selects its asset and leaves the row folded', o2Selection?.kind === 'asset' && o2Selection.assetId === o2Asset && same(await rowIds(), ['o1', 'o2', 'o3', 'o5']), JSON.stringify({ o2Selection, o2Asset, rows: await rowIds() }), ); await select({ kind: 'object', holderId: 'scene1', objectId: 'o5' }); const o5Row = await json(`__gm.hierarchyRow('o5')`); await doubleClick(o5Row.x, o5Row.y); const openedByRow = await json('__gm.documents()'); check( 'double-clicking a selected row opens the tab of its asset', openedByRow.active === 'asset:spawner', JSON.stringify(openedByRow), ); await evaluate(`__gm.closeDocument('asset:spawner'); __gm.state.select(null)`); await select({ kind: 'object', holderId: 'scene1', objectId: 'oc1' }); check( 'selecting a nested object unfolds its ancestors', same(await rowIds(), ['o1', 'o4', 'oc1', 'o2', 'o3', 'o5']), await rows(), ); await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()'); check('Collapse all folds everything back', same(await rowIds(), ['o1', 'o2', 'o3', 'o5']), await rows()); // Open asset in the row's menu does what a double-click in the asset panel does. const spawnerRow = await json(`__gm.hierarchyRow('o5')`); await click(spawnerRow.x, spawnerRow.y, 'right'); await clickText('Open asset'); const opened = await json('__gm.documents()'); const openedSelection = (await state()).selection; check( 'Open asset in the row menu selects the asset and opens its tab', opened.active === 'asset:spawner' && opened.docs.includes('asset:spawner') && openedSelection?.kind === 'asset' && openedSelection.assetId === 'spawner', JSON.stringify({ opened, openedSelection }), ); await evaluate(`__gm.closeDocument('asset:spawner'); __gm.state.select(null)`); // A sprite has no editor of its own, so the entry that promises to open one is greyed out. const crateRow = await json(`__gm.hierarchyRow('o1')`); await click(crateRow.x, crateRow.y, 'right'); const menuRows = await json('__gm.menuRows()'); const menuEnabled = await json('__gm.menuEnabled()'); check( 'Open asset is greyed out for a kind without an editor, Select asset stays on', menuEnabled[menuRows.indexOf('Open asset')] === false && menuEnabled[menuRows.indexOf('Select asset')] === true, JSON.stringify({ menuRows, menuEnabled }), ); await press('Escape'); await evaluate('__gm.state.select(null)'); }