Files
feuerwerk/tools/e2e/07-parenting.mjs
T
rootandClaude Fable 5.1 6471671f7d Refactor round: ship web/index.html, drop the remote browser, fix stale comments and README
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
  a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
  the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
  history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
  nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
  step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
  and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
  and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
  tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.

Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 15:44:05 +02:00

165 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { same } from '../lib/harness.mjs';
/**
* Hierarchy / parenting. Gives both crates unique names (later steps locate them by these),
* then parents by drag, by the inspector menu and unparents from the context menu – the world
* position is kept every time.
*/
export async function parenting(t) {
const {
check,
sleep,
json,
shot,
click,
clickText,
drag,
locate,
typeText,
press,
undo,
mutate,
select,
object,
entity,
stop,
} = t;
// 7a. Drag the blue crate's row onto the red crate's row: it becomes a child, keeps its world
// position (local becomes the difference) and is listed right below its parent.
await mutate(`
const objs = __gm.state.activeScene.objects;
const o1 = objs.find(o => o.id === 'o1'); const o2 = objs.find(o => o.id === 'o2');
o1.name = 'Red crate'; o1.position = { x: -150, y: 0, z: 0 };
o2.name = 'Blue crate'; o2.position = { x: 120, y: -80, z: 0 }; o2.parentId = '';
__gm.state.select(null)`);
const redRow = await locate('Red crate');
const blueRow = await locate('Blue crate');
await drag(blueRow.x, blueRow.y, redRow.x, redRow.y);
await sleep(100);
let o2 = await object('o2');
check(
'dropping a row onto another parents it, keeping the world position',
o2.parentId === 'o1' && o2.position.x === 270 && o2.position.y === -80,
JSON.stringify(o2),
);
const order = await json('(__gm.hierarchyExpandAll(), __gm.hierarchyOrder())');
// o1's subtree: o2 (list order before o4) with its collider child, then o4, then o1's collider.
const o1At = order.indexOf('o1');
check(
'children are listed directly below their parent',
same(order.slice(o1At + 1, o1At + 5), ['o2', 'oc2', 'o4', 'oc1']),
JSON.stringify(order),
);
await shot('20-parented');
// 7b. Moving the parent (X field of the red crate) drags the child along in world space.
await clickText('Red crate');
const xField = await locate('X');
await click(xField.x + 160, xField.y);
await press('a', { ctrl: true });
await typeText('-50');
await press('Enter');
await sleep(200);
o2 = await object('o2');
const parent = await object('o2').then((o) => object(o.parentId));
const worldOfO2 = { x: o2.position.x + parent.position.x, y: o2.position.y + parent.position.y };
check(
'moving the parent moves the child in world space (local unchanged)',
o2.position.x === 270 && same(worldOfO2, { x: 220, y: -80 }),
`${JSON.stringify(o2.position)} world=${JSON.stringify(worldOfO2)}`,
);
// 7c. Play: entities mirror the hierarchy; undo restores parent and position.
await select(null);
await press('Enter', { ctrl: true });
await sleep(300);
const e2 = await entity('o2');
check(
'play instantiates the parent link and world positions',
e2 && e2.parent === 'o1' && e2.worldX === 220 && e2.x === 270,
JSON.stringify(e2),
);
await stop();
await undo(); // move
await undo(); // parent
o2 = await object('o2');
check(
'undo restores top level and the original local position',
o2.parentId === '' && o2.position.x === 120 && o2.position.y === -80,
JSON.stringify(o2),
);
// 7d. Parent via the inspector menu: the picker is searchable, Enter takes the single match.
await clickText('Blue crate');
await clickText('None'); // the Parent dropdown, labelled with the current parent
const entries = await json('__gm.overlayTexts()');
check(
'the parent picker offers a search box and lists the candidates',
entries.includes('Search…') && entries.includes('None') && entries.includes('Red crate'),
JSON.stringify(entries),
);
await typeText('red');
await sleep(150);
const filtered = await json('__gm.overlayTexts()');
check(
'typing filters the picker down to the matching entries',
same(filtered, ['red', 'Red crate']),
JSON.stringify(filtered),
);
await shot('30-parent-search');
await press('Enter'); // takes the first match
await sleep(150);
o2 = await object('o2');
check(
'inspector Parent menu parents the object',
o2.parentId === 'o1' && o2.position.x === 270,
JSON.stringify(o2),
);
check('the picker closed after Enter', same(await json('__gm.overlayTexts()'), []));
const blueRow2 = await locate('Blue crate');
await click(blueRow2.x, blueRow2.y, 'right');
await clickText('Unparent (move to top level)');
await sleep(100);
o2 = await object('o2');
check(
'Unparent moves the object to the top level, keeping its world position',
o2.parentId === '' && o2.position.x === 120 && o2.position.y === -80,
JSON.stringify(o2),
);
// 7e. The gap above a row is a different drop than the row itself: it orders the dragged
// object among that row's siblings instead of putting it inside, and moves nothing.
const rowsBefore = await json('__gm.hierarchyOrder()');
check(
'the blue crate is listed after the red one to begin with',
rowsBefore.indexOf('o2') > rowsBefore.indexOf('o1'),
JSON.stringify(rowsBefore),
);
const redRow3 = await locate('Red crate');
const blueRow3 = await locate('Blue crate');
// 10 px above the centre of a 26 px row is its top quarter – the gap, not the row.
await drag(blueRow3.x, blueRow3.y, redRow3.x, redRow3.y - 10);
await sleep(150);
const rowsAfter = await json('__gm.hierarchyOrder()');
o2 = await object('o2');
check(
'dropping into the gap reorders instead of parenting',
rowsAfter.indexOf('o2') < rowsAfter.indexOf('o1') && o2.parentId === '',
JSON.stringify({ rowsAfter, parentId: o2.parentId }),
);
check(
'and a reorder moves nothing in the world',
o2.position.x === 120 && o2.position.y === -80,
JSON.stringify(o2.position),
);
await shot('31-reordered');
await undo();
await sleep(150);
check(
'undo puts the order back',
same(await json('__gm.hierarchyOrder()'), rowsBefore),
JSON.stringify(await json('__gm.hierarchyOrder()')),
);
}