The CI run for ded71a9 failed three checks in e2e 58 clipAreas. The
cause was the new step 57g, not the editor. It ends in the Variables
tab on purpose, and the clean-up of 57 then deletes the object whose
list that tab shows. The tab closes itself, and the editor falls back
to the last open document instead of the scene view. In the full run
earlier modules had left document tabs open, so 58 measured and
clicked in the wrong tab. Run on its own, 57 had no other document
open, which is why the module run before the push was green.
- 57 switches back to the scene view before its clean-up, with a
comment that says why.
- 58 sets the scene view itself at the start, as 44 and 51 already do.
It no longer depends on what the modules before it left open.
Tests:
- e2e 57,58 with the fix: ALL PASSED, 17 checks.
- Counter-check 57,58 and 54,57,58 without the fix: green, because no
other document was open.
- Counter-check with a graph tab left open before 57, without the fix:
3 FAILED with the same values as in CI. Same setup with the fix:
ALL PASSED, 17 checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
282 lines
12 KiB
JavaScript
282 lines
12 KiB
JavaScript
/**
|
|
* Picking an object of the same scene for an object variable (#258), with the real mouse: the
|
|
* placed behavior offers None and the scene's objects, the pick is stored as the object's id and a
|
|
* run reads the object itself. Removing that object drops the reference; undo brings it back. On the
|
|
* behavior asset there is no scene, so the row still says "(null)".
|
|
*
|
|
* The eyedropper beside the dropdown (#276) picks in the scene view instead: over two stacked
|
|
* objects a click takes the one in front and a second click the one behind, Enter writes it, Escape,
|
|
* a right-click and a second press of the eyedropper leave it as it was. From a list item in a
|
|
* Variables tab it goes to the scene view and comes back with Take.
|
|
*
|
|
* Everything it adds carries an `or57` id and goes again at the end.
|
|
*/
|
|
import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
|
|
|
|
export async function objectReferences(t) {
|
|
const { check, json, evaluate, mutate, click, press, typeText, sleep, shot, select, undo, stop } = t;
|
|
const press_ = async (at, what) => {
|
|
if (!at) throw new Error(`nothing to click: ${what}`);
|
|
await click(at.x, at.y);
|
|
await sleep(150);
|
|
};
|
|
const target = () =>
|
|
json("__gm.state.activeScene.objects.find((o) => o.id === 'or57-obj')?.overrides.target ?? null");
|
|
|
|
// On start the behavior logs the object its `target` names.
|
|
const graph = {
|
|
nodes: [
|
|
{ id: 'or57-start', type: 'OnStart', x: 0, y: 0, params: {} },
|
|
{
|
|
id: 'or57-vars',
|
|
type: 'Variables',
|
|
x: 0,
|
|
y: 200,
|
|
params: {},
|
|
ports: [{ key: 'target', kind: 'object' }],
|
|
},
|
|
{ id: 'or57-log', type: 'Log', x: 320, y: 0, params: {} },
|
|
],
|
|
edges: [
|
|
{ id: 'or57-e1', from: { node: 'or57-start', port: 'exec' }, to: { node: 'or57-log', port: 'exec' } },
|
|
{
|
|
id: 'or57-e2',
|
|
from: { node: 'or57-vars', port: 'target' },
|
|
to: { node: 'or57-log', port: 'message' },
|
|
},
|
|
],
|
|
};
|
|
await mutate(`
|
|
const p = __gm.state.project;
|
|
const scene = __gm.state.activeScene;
|
|
p.assets.push({
|
|
id: 'or57-aim',
|
|
type: 'behavior',
|
|
name: 'Aim 57',
|
|
props: { enabled: true, runInEditor: false, target: { $object: '' } },
|
|
variables: [{ key: 'target', label: 'target', kind: 'object', default: { $object: '' } }],
|
|
graph: ${JSON.stringify(graph)},
|
|
});
|
|
${MAKE_OBJECT}
|
|
const sprite = p.assets.find((a) => a.type === 'sprite');
|
|
scene.objects.push(make('or57-obj', 'or57-aim', '', 400, 'Aim 57'));
|
|
scene.objects.push(make('or57-crate', sprite.id, '', 450, 'Crate 57'));
|
|
`);
|
|
const sceneId = await json('__gm.state.activeScene.id');
|
|
|
|
// 57a. The behavior asset has no scene: its object variable only says "(null)".
|
|
await select({ kind: 'asset', assetId: 'or57-aim' });
|
|
await sleep(200);
|
|
check(
|
|
'on the behavior asset the object variable says (null)',
|
|
(await json("__gm.locate('(null)')")) !== null,
|
|
);
|
|
|
|
// 57b. On the placed object the row is a dropdown: None, the object itself and the Crate. A click
|
|
// on the Crate stores its id.
|
|
await select({ kind: 'object', holderId: sceneId, objectId: 'or57-obj' });
|
|
await sleep(200);
|
|
await press_(await json("__gm.inspectorField('target')"), 'the target field');
|
|
const offered = (await json('__gm.overlayTexts()')) ?? [];
|
|
// In the full suite the scene holds more objects than the menu shows at once, so the two new ones
|
|
// at its end are only reached through the search box.
|
|
await typeText('57');
|
|
const found = (await json('__gm.overlayTexts()')) ?? [];
|
|
await shot('01-menu');
|
|
check(
|
|
'the placed object offers None, itself and the other objects of the scene',
|
|
offered.includes('None') && ['Aim 57', 'Crate 57'].every((label) => found.includes(label)),
|
|
JSON.stringify({ offered, found }),
|
|
);
|
|
await press_(await json("__gm.locate('Crate 57', true)"), 'Crate 57 in the menu');
|
|
const picked = await target();
|
|
check(
|
|
'picking the Crate stores its id and the row shows its name',
|
|
JSON.stringify(picked) === JSON.stringify({ $object: 'or57-crate' }) &&
|
|
(await json("__gm.locate('Crate 57')")) !== null,
|
|
JSON.stringify(picked),
|
|
);
|
|
await shot('02-picked');
|
|
|
|
// 57c. A run reads the object itself.
|
|
await evaluate('__gm.state.select(null)');
|
|
const logFrom = t.consoleLog.length;
|
|
await t.play();
|
|
await sleep(600);
|
|
await stop();
|
|
const logged = t.consoleLog
|
|
.slice(logFrom)
|
|
.filter((line) => line.startsWith('[behavior Aim 57]'))
|
|
.map((line) => line.split(']').pop().trim());
|
|
check('in a run the variable is the picked object', logged.includes('Crate 57'), JSON.stringify(logged));
|
|
|
|
// 57d. Removing the Crate drops the reference with it; one undo brings both back.
|
|
await select({ kind: 'object', holderId: sceneId, objectId: 'or57-crate' });
|
|
await sleep(150);
|
|
await press('Delete');
|
|
await sleep(150);
|
|
const dropped = await target();
|
|
await undo();
|
|
const back = await target();
|
|
check(
|
|
'removing the picked object drops the reference, undo brings it back',
|
|
JSON.stringify(dropped) === JSON.stringify({ $object: '' }) &&
|
|
JSON.stringify(back) === JSON.stringify({ $object: 'or57-crate' }),
|
|
JSON.stringify({ dropped, back }),
|
|
);
|
|
|
|
// 57e. The eyedropper (#276): two objects stacked on a lane of their own, the target back to
|
|
// None, and a list of one object item for 57g.
|
|
const Y = LANES.eyedropper;
|
|
await mutate(`
|
|
const p = __gm.state.project;
|
|
const scene = __gm.state.activeScene;
|
|
${MAKE_OBJECT}
|
|
const sprite = p.assets.find((a) => a.type === 'sprite');
|
|
scene.objects.push(make('or57-back', sprite.id, '', ${Y}, 'Back 57'));
|
|
scene.objects.push(make('or57-front', sprite.id, '', ${Y}, 'Front 57'));
|
|
const aim = p.assets.find((a) => a.id === 'or57-aim');
|
|
aim.variables.push({ key: 'crowd', label: 'crowd', kind: 'list', default: [] });
|
|
aim.props.crowd = [];
|
|
const obj = scene.objects.find((o) => o.id === 'or57-obj');
|
|
obj.overrides.target = { $object: '' };
|
|
obj.overrides.crowd = [{ $object: '' }];
|
|
`);
|
|
await select({ kind: 'object', holderId: sceneId, objectId: 'or57-obj' });
|
|
await evaluate(`__gm.lookAt(0, ${Y})`);
|
|
await sleep(200);
|
|
const pick = () => json('__gm.objectPick()');
|
|
const eyedropper = () => json("__gm.inspectorEyedropper('target')");
|
|
const inInspector = async (text) => {
|
|
const box = await json('__gm.inspectorRect()');
|
|
const hits = (await json(`__gm.locateAll(${JSON.stringify(text)})`)) ?? [];
|
|
return hits.some((p) => p.x >= box.x && p.x <= box.x + box.w && p.y >= box.y && p.y <= box.y + box.h);
|
|
};
|
|
const spot = await json("__gm.objectScreen('or57-front')");
|
|
const stack = await json(`__gm.sceneStack(${spot.x}, ${spot.y})`);
|
|
|
|
await press_(await eyedropper(), 'the eyedropper of target');
|
|
const started = await pick();
|
|
const lit = (await eyedropper())?.lit;
|
|
check(
|
|
'the eyedropper starts in the scene view, lit, and the bar over it names the field',
|
|
started.picking &&
|
|
started.tab === 'scene' &&
|
|
lit === true &&
|
|
started.candidate === null &&
|
|
started.strip?.text === 'Target for Aim 57 · target: click an object',
|
|
JSON.stringify({ started, lit }),
|
|
);
|
|
|
|
await click(spot.x, spot.y);
|
|
const first = await pick();
|
|
await click(spot.x, spot.y);
|
|
const second = await pick();
|
|
await shot('03-eyedropper');
|
|
const empty = await json('__gm.emptySpot()');
|
|
await click(empty.x, empty.y);
|
|
const afterEmpty = await pick();
|
|
check(
|
|
'a click takes the object in front, a second one the object behind, empty space keeps it, nothing is written',
|
|
JSON.stringify(stack) === JSON.stringify(['or57-front', 'or57-back']) &&
|
|
first.candidate === 'or57-front' &&
|
|
second.candidate === 'or57-back' &&
|
|
second.outline === 'or57-back' &&
|
|
second.strip?.text === 'Target for Aim 57 · target: Back 57' &&
|
|
afterEmpty.candidate === 'or57-back' &&
|
|
afterEmpty.picking &&
|
|
JSON.stringify(await target()) === JSON.stringify({ $object: '' }),
|
|
JSON.stringify({ stack, first, second, afterEmpty }),
|
|
);
|
|
|
|
await press('Enter');
|
|
const taken = await pick();
|
|
const written = await target();
|
|
const shown = await inInspector('Back 57');
|
|
const litAfter = (await eyedropper())?.lit;
|
|
await undo();
|
|
const undone = await target();
|
|
check(
|
|
'Enter writes the candidate, the row shows it, the eyedropper goes off, one undo takes it back',
|
|
!taken.picking &&
|
|
taken.ended === 'take' &&
|
|
taken.strip === null &&
|
|
JSON.stringify(written) === JSON.stringify({ $object: 'or57-back' }) &&
|
|
shown &&
|
|
litAfter === false &&
|
|
JSON.stringify(undone) === JSON.stringify({ $object: '' }),
|
|
JSON.stringify({ taken, written, shown, litAfter, undone }),
|
|
);
|
|
|
|
// 57f. Escape, a right-click in the view and a second press of the eyedropper leave it as it was.
|
|
const cancels = [];
|
|
for (const [how, leave] of [
|
|
['Escape', () => press('Escape')],
|
|
['right-click', () => click(spot.x, spot.y, 'right')],
|
|
['eyedropper', async () => press_(await eyedropper(), 'the eyedropper, again')],
|
|
]) {
|
|
await press_(await eyedropper(), 'the eyedropper of target');
|
|
await click(spot.x, spot.y);
|
|
const before = (await pick()).candidate;
|
|
await leave();
|
|
const after = await pick();
|
|
const menu = (await json('__gm.overlayTexts()')) ?? [];
|
|
cancels.push({ how, before, picking: after.picking, ended: after.ended, menu, target: await target() });
|
|
}
|
|
check(
|
|
'Escape, a right-click and a second press of the eyedropper end it without writing',
|
|
cancels.every(
|
|
(c) =>
|
|
c.before === 'or57-front' &&
|
|
!c.picking &&
|
|
c.ended === 'cancel' &&
|
|
c.menu.length === 0 &&
|
|
JSON.stringify(c.target) === JSON.stringify({ $object: '' }),
|
|
),
|
|
JSON.stringify(cancels),
|
|
);
|
|
|
|
// 57g. A list item in a Variables tab: the eyedropper goes to the scene view, Take writes the item
|
|
// and brings the tab back.
|
|
await evaluate(
|
|
`__gm.state.openValues({ kind: 'list', slot: { kind: 'object', holderId: ${JSON.stringify(sceneId)}, objectId: 'or57-obj', key: 'crowd' } })`,
|
|
);
|
|
await sleep(200);
|
|
const tabShown = (await pick()).tab;
|
|
const inspectorLeft = (await json('__gm.inspectorRect()')).x;
|
|
const inTab = ((await json("__gm.locateIcons('eyedropper')")) ?? []).filter((p) => p.x < inspectorLeft);
|
|
await press_(inTab[0], 'the eyedropper of the list item');
|
|
const fromList = await pick();
|
|
await click(spot.x, spot.y);
|
|
const listCandidate = (await pick()).candidate;
|
|
const take = (await pick()).strip?.take;
|
|
await press_(take, 'Take');
|
|
const backIn = await pick();
|
|
const crowd = await json(
|
|
"__gm.state.activeScene.objects.find((o) => o.id === 'or57-obj')?.overrides.crowd ?? null",
|
|
);
|
|
check(
|
|
'from a list item in a Variables tab it picks in the scene view, and Take writes it and goes back',
|
|
tabShown === 'variables' &&
|
|
inTab.length === 1 &&
|
|
fromList.tab === 'scene' &&
|
|
fromList.strip?.text === 'Target for Aim 57 · crowd · [0]: click an object' &&
|
|
listCandidate === 'or57-front' &&
|
|
backIn.ended === 'take' &&
|
|
backIn.tab === 'variables' &&
|
|
JSON.stringify(crowd) === JSON.stringify([{ $object: 'or57-front' }]),
|
|
JSON.stringify({ tabShown, inTab, fromList, listCandidate, backIn, crowd }),
|
|
);
|
|
|
|
// Clean up. Back to the scene view first: removing Aim 57 closes 57g's Variables tab, and the
|
|
// editor then shows whatever document earlier steps left open, not the scene view.
|
|
await t.showTab('scene');
|
|
await evaluate('__gm.state.select(null)');
|
|
await mutate(`
|
|
const p = __gm.state.project;
|
|
const scene = __gm.state.activeScene;
|
|
scene.objects = scene.objects.filter((o) => !o.id.startsWith('or57-'));
|
|
p.assets = p.assets.filter((a) => a.id !== 'or57-aim');
|
|
`);
|
|
}
|