Files
feuerwerk/tools/e2e/57-object-references.mjs
T
adminandClaude Opus 5.5 6c62aa45d5 Pick Crate 57 through the menu search in e2e 57 (#258)
CI has been red since f84defb. In the full run the scene holds more
than 40 filler objects from module 11. The object menu is height
limited and scrolls, and overlayTexts() only returns the visible rows.
Aim 57 and Crate 57 sit at the end of the scene and were never visible.

- Check None right after opening the menu, it is always the top row.
- Type 57 into the search field before looking for Aim 57 and
  Crate 57 and before clicking Crate 57, like module 7 does with the
  parent picker.
- No product code changes.

Tests:
- Reproduced with modules 1-11 and 57: the old test fails with the
  same cut-off list and "nothing to click: Crate 57" as CI.
- New test: ALL PASSED - 405 checks, all 5 checks of 57 green.
- The full run could not be verified locally. Module 12 fails here
  because the local server holds leftover uploaded images. It is
  green in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:57:37 +02:00

132 lines
5.1 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)".
*
* Everything it adds carries an `or57` id and goes again at the end.
*/
import { 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 }),
);
// Clean up.
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');
`);
}