Files
feuerwerk/tools/e2e/10-text-and-enabled.mjs
T
admin 7e5003b590
game-maker / suite (push) Successful in 6m48s
A test that waits for a deadline somebody guessed is waiting for the wrong thing, so it waits for the editor (#93)
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.

The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.

Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.

564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.

Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
2026-09-16 08:08:10 +02:00

111 lines
5.1 KiB
JavaScript
Raw 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.
/** Text assets rendered in the scene, and the enabled switch in the editor and in the running game. */
export async function textAndEnabled(t) {
const {
check,
evaluate,
json,
has,
shot,
click,
clickText,
locate,
mutate,
object,
play,
press,
stop,
typeText,
} = t;
// 10a. Text assets: created through the + menu, placed, and rendered as real text in the scene.
await clickText('+');
await clickText('Text');
const textAssetId = await evaluate(`(__gm.state.project.assets.find(a => a.type === 'text') || {}).id`);
check('+ menu creates a text asset', Boolean(textAssetId));
await mutate(`
const id = ${JSON.stringify(textAssetId)};
__gm.state.project.assets.find(a => a.id === id).props.text = 'Hello world';
// On the camera so the game view really renders it (outside the frame it would be culled).
const cam = __gm.state.activeScene.objects.find(o => o.id === 'o3').position;
__gm.state.activeScene.objects.push({ id: 'otext', assetId: id, name: '', position: { x: cam.x, y: cam.y, z: 5 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: '', instanceEdits: {} });
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
check('a placed text object draws its content in the scene', await has('Hello world'));
await shot('29-text-object');
// 10b. The enabled switch: every object has it, switching it off is a normal override, and a
// switched-off object disappears from the scene view too (it stays in the hierarchy).
const enabledRow = await json("__gm.inspectorField('Enabled')");
check('the inspector shows the Enabled switch for a scene object', Boolean(enabledRow));
const visibleBefore = (await json('__gm.viewportInfo()')).visibleItems;
const gizmoOf = async (id) => (await json('__gm.gizmos()')).find((g) => g.id === id) ?? null;
const gizmoOn = await gizmoOf('otext');
check('a selected object is framed at full strength', gizmoOn?.alpha === 1, JSON.stringify(gizmoOn));
await click(enabledRow.x, enabledRow.y);
const textObject = await object('otext');
check(
'clicking it writes an override instead of changing the asset',
textObject.overrides.enabled === false,
JSON.stringify(textObject.overrides),
);
check(
'a switched-off object is not drawn in the scene view',
!(await json('__gm.drawOrder()')).includes('otext'),
);
const visibleAfter = (await json('__gm.viewportInfo()')).visibleItems;
check(
'the scene view renders one item less',
visibleAfter === visibleBefore - 1,
`${visibleBefore} → ${visibleAfter}`,
);
check('it stays selectable in the hierarchy, so its switch stays reachable', await has('Enabled'));
await shot('30-disabled');
// 17a-2. It draws nothing, so its outline is the only thing left that says where it sits – which
// is the whole point of being able to pick it in the hierarchy. Selecting another object
// and coming back proves the outline follows the panel, not a leftover from before.
const gizmoOff = await gizmoOf('otext');
check(
'a switched-off object is still framed, dimmed',
gizmoOff?.primary === true && gizmoOff.alpha > 0 && gizmoOff.alpha < 1,
JSON.stringify(gizmoOff),
);
const otherRow = await evaluate(`__gm.hierarchyRow('o3')`);
await click(otherRow.x, otherRow.y);
check('selecting something else drops the frame', (await gizmoOf('otext')) === null);
const offRow = await evaluate(`__gm.hierarchyRow('otext')`);
await click(offRow.x, offRow.y);
const gizmoBack = await gizmoOf('otext');
check(
'picking the switched-off object in the hierarchy frames it again',
gizmoBack?.alpha > 0 && gizmoBack.alpha < 1,
JSON.stringify(gizmoBack),
);
check('and it is still not drawn', !(await json('__gm.drawOrder()')).includes('otext'));
await shot('30b-disabled-gizmo');
// 10c. In the running game a switched-off object does not exist: it is not rendered, and
// switching it back on brings it back.
await play();
const offItems = (await json('__gm.gameInfo()')).visibleItems;
await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'otext').overrides.enabled = true`);
const onItems = (await json('__gm.gameInfo()')).visibleItems;
check(
'the game renders one item more once the object is switched on',
onItems === offItems + 1,
`${offItems} → ${onItems}`,
);
await stop();
check('stop leaves the play-mode switch behind', (await object('otext')).overrides.enabled === false);
// 10d. A field that shows no decimals must not store any either. Font size is such a field, so
// typing a fraction has to round: showing "33" while the project holds 32.6 would be a lie.
const fontLabel = await locate('Font size');
check('Font size reads as a whole number', Boolean(await has('32')) && !(await has('32.000')));
await click(fontLabel.x + 160, fontLabel.y);
await typeText('32.6');
await press('Enter');
const fontSize = (await object('otext')).overrides.fontSize;
check('typing a fraction into it rounds the stored value', fontSize === 33, String(fontSize));
check('and the field shows exactly what was stored', Boolean(await has('33')));
}