The asset link in the inspector already pings an asset on a click and opens it on a double-click. The rows of the Scene objects panel only selected and folded. Now they lead to the asset the same way. - A click on the row that is selected already pings the object's asset in the asset panel. The selection stays. The ping comes on release, so picking a row up to drag it pings nothing. - A double-click or Enter on an object's row opens its asset, selected or not. - ListView has a new onReselect for a plain click on the row that is selected alone. onSelect stays quiet then, as nothing changes. Decided without asking: - A double-click on an object's row no longer folds it. The chevron and the arrow keys still do. A group's row has no asset and keeps folding on a double-click. - A click with Ctrl or Shift, and one that narrows a multi-selection to its row, does not ping. Both change the selection. - A row whose asset is missing pings and opens nothing. Findings: - The double-click window goes by time (400 ms), not by the event's detail. Two quick click() calls from the harness are a double-click. The comment in e2e 01 said otherwise and is gone. Tests: - e2e 01 (melektaus-atx): ALL PASSED, 13 checks. The two checks for folding on a double-click became one for the ping and two for opening. Counter-check without the ping: that check red. - Gate (melektaus-atx): CHECK EXIT 0, 268 files, 4142 tests. - Not checked: Enter on a row, a group's row on a double-click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
5.3 KiB
JavaScript
119 lines
5.3 KiB
JavaScript
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 – it no longer folds the row.
|
||
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)');
|
||
}
|