/** * Selecting, renaming and scrubbing in the inspector, dragging in the viewport, autosave to * the server, and undo/redo of all of it. */ export async function inspector(t) { const { check, sleep, json, locate, shot, click, doubleClick, clickText, drag, press, typeText, mutate, undo, redo, state, object, api, sceneOf, projectId, } = t; // 2a. Select the blue crate in the hierarchy. await clickText('Blue crate'); let s = await state(); check( 'hierarchy click selects object', s.selection?.kind === 'object' && s.selection.objectId === 'o2', JSON.stringify(s.selection), ); await shot('02-object-selected'); // 2b. Deviations are shown in gold – on the inspector label and on the hierarchy row. The blue // crate overrides width and color, the camera and the red crate's subtree nothing. const GOLD = '#f2c84b'; const PLAIN = '#e6e8ee'; const rowColor = (id) => json(`__gm.hierarchyRowColor('${id}')`); check('an overridden object is marked gold in the hierarchy', (await rowColor('o2')) === GOLD); check('an untouched object is not', (await rowColor('o3')) === PLAIN, await rowColor('o3')); check('nor is a parent whose subtree is untouched', (await rowColor('o1')) === PLAIN); check( 'the overridden field is gold, an inherited one is not', (await json(`__gm.textColor('Width')`)) === GOLD && (await json(`__gm.textColor('Height')`)) === '#8a90a0', JSON.stringify(await json(`[__gm.textColor('Width'), __gm.textColor('Height')]`)), ); // The mark also travels up: with the crate's own overrides taken away the row must stay gold // for the sake of its collider child, which keeps one. Put back right after, byte for byte. await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'o2').overrides = {}`); check('a parent carries the mark of a deviating child', (await rowColor('o2')) === GOLD); await mutate( `__gm.state.activeScene.objects.find(o => o.id === 'o2').overrides = { width: 140, color: 0x4488ff }`, ); // 2c. Rename via inspector: the name field sits right of the "Name" label. const nameLabel = await locate('Name'); await click(nameLabel.x + 160, nameLabel.y); await press('a', { ctrl: true }); await typeText('Renamed crate'); await shot('03-typing'); await press('Enter'); await sleep(200); check( 'typing + Enter renamed the object', (await object('o2')).name === 'Renamed crate', JSON.stringify((await object('o2')).name), ); await shot('04-renamed'); // 2d. A double-click into a text field selects its content, so the next keystroke replaces the // whole name instead of inserting into it. Renamed back, since later checks go by the name. await doubleClick(nameLabel.x + 160, nameLabel.y); await typeText('Twice'); await press('Enter'); await sleep(200); check( 'double-clicking a text field selects all – typing replaces the name', (await object('o2')).name === 'Twice', JSON.stringify((await object('o2')).name), ); // Undone rather than typed back: check 2h further down counts its undos, so this must not // leave a step on the stack. await undo(); await sleep(200); check( 'undo takes that rename back', (await object('o2')).name === 'Renamed crate', JSON.stringify((await object('o2')).name), ); // 2e. Scrub the X field (right of the "X" label) by 100 px. const xLabel = await locate('X'); await drag(xLabel.x + 160, xLabel.y, xLabel.x + 260, xLabel.y); const xNow = (await object('o2')).position.x; check('scrubbing X moved the object', xNow > 120, `x=${xNow}`); await shot('05-scrubbed'); // 2f. Type into X, then click straight into the empty viewport: the typed value must be // committed (not reverted) even though the click deselects and rebuilds the inspector. const center = await json('__gm.viewportCenter()'); await click(xLabel.x + 160, xLabel.y); // plain click → text editing with everything selected await typeText('300'); await click(center.x, center.y + 250); const xTyped = (await object('o2')).position.x; s = await state(); check( 'typed X survives clicking into the viewport', xTyped === 300 && s.selection === null, `x=${xTyped} selection=${JSON.stringify(s.selection)}`, ); // 2g. Drag the red crate in the viewport. It is at world (-150,0); the view is centred // on the origin, so compute its screen position from the viewport centre. await drag(center.x - 150, center.y, center.x - 50, center.y - 70); const o1Now = (await object('o1')).position; check('viewport drag moved the red crate', o1Now.x > -150, JSON.stringify(o1Now)); await shot('06-dragged'); await sleep(1800); const saved = await api(`projects/${projectId}`); const renamed = sceneOf(saved).objects.find((o) => o.id === 'o2'); check('rename persisted', renamed.name === 'Renamed crate', renamed.name); check('scrub persisted', renamed.position.x > 120, JSON.stringify(renamed.position)); check('override kept', renamed.overrides.color === 4491519, JSON.stringify(renamed.overrides)); check('save status is saved', (await state()).save === 'saved'); // 2h. Undo four times (drag, typed X, scrub, rename) with Ctrl+Z, then redo once. await click(center.x, center.y + 200); // click empty viewport → deselect, drop focus for (let i = 0; i < 4; i++) await undo(); const afterUndo = (await t.objects()).map((o) => ({ id: o.id, name: o.name, x: o.position.x })); check( 'undo reverted drag, typed X, scrub and rename', afterUndo[0].x === -150 && afterUndo[1].x === 120 && afterUndo[1].name === 'Blue crate', JSON.stringify(afterUndo), ); await redo(); check('redo restored the rename', (await object('o2')).name === 'Renamed crate'); await shot('07-undo-redo'); // 2i. A change to the *structure* of what is shown normally rebuilds the inspector, which // destroys the widget being typed in. While a field in here has focus the rebuild has to // wait – otherwise a field whose value decides the structure cannot be typed into at all. await clickText('Renamed crate'); const xRow = await locate('X'); await click(xRow.x + 160, xRow.y); // The value it already has: Escape cancels the widget but not the preview it wrote, so // typing anything else here would leave that value behind for the later steps. await typeText('120'); const before = await json('__gm.inspectorInfo()'); // The parent is part of the object's structure signature, so this asks for a rebuild. await mutate("__gm.state.activeScene.objects.find(o => o.id === 'o2').parentId = 'o1'"); const during = await json('__gm.inspectorInfo()'); const stillTyping = await json( '({ tag: document.activeElement?.tagName, value: document.activeElement?.value })', ); check( 'a structure change while typing does not rebuild the inspector', during.rebuilds === before.rebuilds && during.postponed, JSON.stringify(during), ); check( 'the field being typed in survives it', stillTyping.tag === 'INPUT' && stillTyping.value === '120', JSON.stringify(stillTyping), ); await press('Escape'); await sleep(200); const after = await json('__gm.inspectorInfo()'); check( 'leaving the field runs the postponed rebuild', after.rebuilds === before.rebuilds + 1 && !after.postponed, JSON.stringify(after), ); await mutate("__gm.state.activeScene.objects.find(o => o.id === 'o2').parentId = ''"); }