Files
feuerwerk/tools/e2e/56-private-variables.mjs
adminandClaude Opus 5.5 a40e629fde
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Successful in 17m24s
Make e2e 56 pass on its own, not only after module 55 (#307)
Check 56a-ii clicks the read-only value of a private variable and types
into it. A read-only field takes no focus, so typeText found no input
and threw when the module ran alone. In the full run the keystroke went
into whatever input an earlier module had left focused, and the check
was green by luck.

- New harness helper typeBlind sends the keystrokes without checking
  where they land. It is meant for keystrokes that should be refused.
- 56a-ii now also checks that no field holds the focus after the click
  (__gm.focusedRect() is null), the same way module 28 checks its
  read-only Asset row.
- No other module types into a field that is meant to be locked.

Tests:
- e2e 56 alone: ALL PASSED, 10 checks (was E2E EXIT 1 before)
- e2e 55,56: ALL PASSED, 27 checks
- Counter-check with the second lock click left out: 56a-ii fails
  with a focused field and timer 9, three later checks follow
- Gate: 229 files, 3514 tests, CHECK EXIT 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:16:59 +02:00

210 lines
8.7 KiB
JavaScript
Raw Permalink 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.
/**
* Private behavior variables (#257), with the real mouse: the lock next to a variable's Keep hides it
* from the placed object – no row, no override left – and from the `+ Property` menu of a
* `Properties` node. A port that names it by hand is still a property of the behavior, and in a run
* it reads the value. The row dialog of an animation does not offer it either (#270).
*
* Everything it adds carries a `pv56` id and goes again at the end.
*/
import { MAKE_OBJECT, openTab } from './lib/scaffold.mjs';
export async function privateVariables(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 asset = (id) => json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)}) ?? null`);
const brain = () => asset('pv56-brain');
const overrides = () =>
json("__gm.state.activeScene.objects.find((o) => o.id === 'pv56-obj')?.overrides ?? null");
/** The locks in the inspector, top to bottom – one per variable. */
const locks = async () => ((await json("__gm.locateIcons('private')")) ?? []).sort((a, b) => a.y - b.y);
const inspectorHas = async (key) => (await json(`__gm.inspectorField(${JSON.stringify(key)})`)) !== null;
const node = (id, type, x, y, params = {}, ports) => ({
id,
type,
x,
y,
params,
...(ports ? { ports } : {}),
});
const edge = (id, from, fromPort, to, toPort) => ({
id,
from: { node: from, port: fromPort },
to: { node: to, port: toPort },
});
// On start, a Properties node of type Brain 56 reads `timer` off the behavior itself – a port
// named by hand, since the menu does not offer it once the variable is private.
const graph = {
nodes: [
node('pv56-start', 'OnStart', 0, 0),
node('pv56-self', 'Self', 0, 200),
node('pv56-props', 'Properties', 240, 200, { objectType: 'behavior', objectBehavior: 'pv56-brain' }, [
{ key: 'timer', kind: 'number' },
]),
node('pv56-log', 'Log', 520, 0),
],
edges: [
edge('pv56-e1', 'pv56-start', 'exec', 'pv56-log', 'exec'),
edge('pv56-e2', 'pv56-self', 'object', 'pv56-props', 'object'),
edge('pv56-e3', 'pv56-props', 'timer', 'pv56-log', 'message'),
],
};
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;
p.assets.push({
id: 'pv56-brain',
type: 'behavior',
name: 'Brain 56',
props: { enabled: true, runInEditor: false, hp: 3, timer: 42 },
variables: [
{ key: 'hp', label: 'hp', kind: 'number', default: 3 },
{ key: 'timer', label: 'timer', kind: 'number', default: 42 },
],
graph: ${JSON.stringify(graph)},
});
${MAKE_OBJECT}
const placed = make('pv56-obj', 'pv56-brain', '', 400, 'Brain 56');
placed.overrides = { hp: 5, timer: 7 };
scene.objects.push(placed);
`);
const sceneId = await json('__gm.state.activeScene.id');
// 56a. Each variable has a lock, off. A click on the one of `timer` makes it private and takes the
// placed object's override of it; undo brings both back.
await select({ kind: 'asset', assetId: 'pv56-brain' });
await sleep(200);
const shown = await locks();
check('each variable of the behavior has a lock', shown.length === 2, JSON.stringify(shown));
await press_(shown[1], 'the lock of timer');
const made = { hidden: (await brain()).privateVariables ?? null, overrides: await overrides() };
check(
'a click on the lock makes timer private and drops its override, hp keeps its own',
JSON.stringify(made) === JSON.stringify({ hidden: ['timer'], overrides: { hp: 5 } }),
JSON.stringify(made),
);
await shot('01-lock');
await undo();
const undone = { hidden: (await brain()).privateVariables ?? null, overrides: await overrides() };
check(
'undo makes it public again, with the override back',
JSON.stringify(undone) === JSON.stringify({ hidden: null, overrides: { hp: 5, timer: 7 } }),
JSON.stringify(undone),
);
await press_((await locks())[1], 'the lock of timer, again');
// 56a-ii. While locked the value is read-only: a click into it leaves no field focused, so typing
// changes nothing. hp still takes a value.
await sleep(150);
await press_(await json("__gm.inspectorField('timer')"), 'timer value');
const focusedAfter = await json('__gm.focusedRect()');
await t.typeBlind('9');
await press('Enter');
await press_(await json("__gm.inspectorField('hp')"), 'hp value');
await press('a', { ctrl: true });
await typeText('8');
await press('Enter');
await sleep(150);
const defaults = (await brain()).props;
check(
'the value of the private timer is read-only and takes no focus, hp still takes a new one',
focusedAfter === null && defaults.timer === 42 && defaults.hp === 8,
JSON.stringify({ focusedAfter, ...defaults }),
);
// 56b. The placed object shows hp only; its Variables tab builds the same rows.
await select({ kind: 'object', holderId: sceneId, objectId: 'pv56-obj' });
await sleep(200);
const onObject = { hp: await inspectorHas('hp'), timer: await inspectorHas('timer') };
check(
'the placed object shows hp, and no row for the private timer',
onObject.hp && !onObject.timer,
JSON.stringify(onObject),
);
await shot('02-object');
// 56c. + Property of a Properties node on Brain 56 offers hp, not timer; the port that names timer
// by hand is no stranger to the behavior, so it carries no warning.
await select({ kind: 'node', assetId: 'pv56-brain', nodeId: 'pv56-props', nodeIds: ['pv56-props'] });
await sleep(200);
await press_(await json("__gm.locate('+ Property', true)"), '+ Property');
const offered = (await json('__gm.overlayTexts()')) ?? [];
await press('Escape');
const warned = (await json(`__gm.locate('"timer" is not a property of "Brain 56".')`)) !== null;
check(
'the Properties menu offers hp but not timer, and the hand-named timer port is not flagged',
offered.includes('hp') && !offered.includes('timer') && !warned,
JSON.stringify({ offered, warned }),
);
await shot('03-properties');
// 56d. In a run that port reads the private value.
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 Brain 56]'))
.map((line) => line.split(']').pop().trim());
check(
'in a run the hand-named port reads the private timer',
logged.includes('42'),
JSON.stringify(logged),
);
// 56e. The row dialog of an animation over the scene offers hp for the placed behavior, and neither
// private variable (#270) – `mood` has no row, so it is not the "already has a row" rule that
// hides it. The row that aimed at timer before it went private stays, label and all.
await mutate(`
const p = __gm.state.project;
const brain = p.assets.find((a) => a.id === 'pv56-brain');
brain.variables.push({ key: 'mood', label: 'mood', kind: 'number', default: 1 });
brain.props.mood = 1;
brain.privateVariables = ['timer', 'mood'];
p.assets.push({
id: 'pv56-walk',
type: 'animation',
name: 'Walk 56',
props: { fps: 10, playback: 'loop' },
root: ${JSON.stringify(sceneId)},
tracks: [{ id: 'pv56-row', target: 'pv56-obj', property: 'timer', keys: [] }],
});
__gm.state.select({ kind: 'asset', assetId: 'pv56-walk' });
`);
await openTab(t, 'animation');
await sleep(300);
await press_(await json('__gm.animation.addRowAt()'), '+ Row');
await evaluate("__gm.animation.rowDialogPick('pv56-obj', 'hp')");
await sleep(150);
const dialog = await json('__gm.animation.rowDialog()');
const rows = await json('__gm.animation.rows()');
const properties = dialog?.properties ?? [];
check(
'the row dialog offers hp and no private variable, the row on the private timer stays',
properties.includes('hp') &&
!properties.includes('mood') &&
!properties.includes('timer') &&
dialog.ok === true &&
JSON.stringify(rows) === JSON.stringify(['Brain 56 · timer']),
JSON.stringify({ properties, ok: dialog?.ok, rows }),
);
await shot('04-row-dialog');
await press_(await json("__gm.locate('Cancel', true)"), 'Cancel');
// 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 !== 'pv56-obj');
p.assets = p.assets.filter((a) => a.id !== 'pv56-brain' && a.id !== 'pv56-walk');
`);
await openTab(t, 'scene');
}