Pixi only fills `detail` on the `click` event it synthesises; on `pointerdown` it is always 0, so every `e.detail === 2` branch was dead code. Asset rows, hierarchy folding and select-all in text fields never responded to a double-click. A shared click counter in src/ui/clicks.ts replaces the three readings of `e.detail`. A finished double-click ends the run, so two in a row count 2 and 2 rather than 3 and 4. The harness could only send `clickCount: 1`, which is why no test caught this: every "activate" check went through Enter. It can send real double-clicks now, and five checks drive the three paths separately.
146 lines
5.8 KiB
JavaScript
146 lines
5.8 KiB
JavaScript
/**
|
||
* 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;
|
||
|
||
// 1. 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');
|
||
|
||
// 1b. 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 }`,
|
||
);
|
||
|
||
// 2. 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');
|
||
|
||
// 2b. 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 6 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),
|
||
);
|
||
|
||
// 3. 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');
|
||
|
||
// 3b. 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)}`,
|
||
);
|
||
|
||
// 4. 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');
|
||
|
||
// 5. Autosave → server.
|
||
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');
|
||
|
||
// 6. 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');
|
||
}
|