Files
feuerwerk/tools/e2e/14-overflow.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

160 lines
6.7 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.
import { TAB_LABELS } from './lib/scaffold.mjs';
/**
* Labels must be shortened to fit their control, not written past it. Runs last because it renames
* assets to absurd lengths to provoke the case.
*/
/** A name far wider than any 200 px picker, so a control that does not truncate must spill. */
const LONG_NAME = 'Player movement with jumping and coyote time';
export async function overflow(t) {
const { check, sleep, json, evaluate, shot, mutate, drag, clickText, showTab } = t;
const pickerWidths = async () => {
const out = {};
for (const tab of ['graph', 'texture', 'container']) {
await showTab(tab);
out[tab] = await json(`__gm.${tab}.pickerWidth()`);
}
return out;
};
const before = await pickerWidths();
await mutate(`
for (const asset of __gm.state.project.assets) {
if (asset.type === 'behavior' || asset.type === 'texture' || asset.type === 'container')
asset.name = ${JSON.stringify(LONG_NAME)};
}
__gm.state.select(null)`);
await sleep(600);
// 14a. The pickers keep the width they were given instead of growing with the name they show.
const after = await pickerWidths();
for (const tab of ['graph', 'texture', 'container']) {
check(
`the ${tab} picker keeps its width when the name gets long`,
before[tab] === 200 && after[tab] === 200,
`before ${before[tab]}, after ${after[tab]}`,
);
}
// 14b. The tab row itself: a button past the centre column's edge sits under the inspector, and
// a click on it lands there (nine tabs on a 90 px floor did exactly that at 1280 px wide).
await showTab('scene');
const columnRight = async () => {
const column = await json('__gm.viewportRect()');
return column.x + column.w;
};
const spillOf = (row, right) => row.shown.filter((b) => b.right > right);
const wide = await json('__gm.tabRow()');
check(
'every tab button ends inside the centre column',
spillOf(wide, await columnRight()).length === 0,
JSON.stringify({ spilled: spillOf(wide, await columnRight()), hidden: wide.hidden }),
);
// The tenth tab (Particles, #73) is the case #84 was built for: 631 px of nine tabs already left
// 35 px in a 1280 px window, so at ten the row has to put something away even undisturbed.
check(
'at the test window’s width the row already needs its overflow, and still shows what is edited',
wide.hidden.length > 0 && wide.shown.some((b) => b.tab === 'scene'),
JSON.stringify({ hidden: wide.hidden, shown: wide.shown.map((b) => b.tab) }),
);
// 14b-ii. Narrowed until the row cannot hold nine tabs, the leftovers go behind the overflow
// button instead of under the inspector – the guard against the tenth tab (#84).
const widenPanels = async (by) => {
const bars = await json('__gm.panelSplitters()');
await drag(bars.left.x, bars.left.y, bars.left.x + by, bars.left.y, { steps: 8 });
const after = await json('__gm.panelSplitters()');
await drag(after.right.x, after.right.y, after.right.x - by, after.right.y, { steps: 8 });
};
await widenPanels(250);
const narrow = await json('__gm.tabRow()');
const narrowRight = await columnRight();
check(
'a row too narrow for its tabs puts the leftovers away instead of past the edge',
narrow.hidden.length > 0 && spillOf(narrow, narrowRight).length === 0,
JSON.stringify({ hidden: narrow.hidden, spilled: spillOf(narrow, narrowRight) }),
);
check(
'the tab being edited stays in the row',
narrow.shown.some((b) => b.tab === 'scene'),
JSON.stringify(narrow.shown.map((b) => b.tab)),
);
await shot('35-tab-overflow');
// The overflow button names them, and picking one brings it into the row.
const wanted = narrow.hidden[narrow.hidden.length - 1];
await clickText('»');
const offered = await json('__gm.overlayTexts()');
check(
'the overflow button offers every tab that had to go',
narrow.hidden.every((tab) => offered.includes(TAB_LABELS[tab])),
JSON.stringify({ offered, hidden: narrow.hidden }),
);
await clickText(TAB_LABELS[wanted]);
const picked = await json('__gm.tabRow()');
check(
`picking ${TAB_LABELS[wanted]} from the overflow menu opens it and puts it in the row`,
(await json('__gm.tab()')) === wanted && picked.shown.some((b) => b.tab === wanted),
JSON.stringify({ tab: await json('__gm.tab()'), shown: picked.shown.map((b) => b.tab) }),
);
await widenPanels(-250);
await showTab('scene');
const back = await json('__gm.tabRow()');
check(
'giving the room back brings tabs into the row again, and none past the edge',
back.hidden.length < narrow.hidden.length && spillOf(back, await columnRight()).length === 0,
JSON.stringify({ hidden: back.hidden, narrow: narrow.hidden }),
);
// 14c. Every tab, because each one owns a different set of controls.
for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'list', 'particles']) {
await showTab(tab);
const spills = await json('__gm.textOverflows()');
check(
`no label is drawn past its control in the ${tab} tab`,
spills.length === 0,
JSON.stringify(spills),
);
}
// 14d. Shortened, not simply dropped: the value must still be readable and marked as cut.
await showTab('graph');
const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement'));
check(
'a value too wide for its dropdown is cut with an ellipsis',
Boolean(shown) && shown.text.endsWith('…') && shown.text.length > 8,
JSON.stringify(shown),
);
await shot('36-long-labels');
// 14e. An inspector full of dropdowns – the parent row and the enum fields – with a selection
// whose asset name is now overlong too.
await showTab('scene');
await evaluate("__gm.state.select({ kind: 'object', objectId: 'otex' })");
await sleep(500);
const inspector = await json('__gm.textOverflows()');
check('no label spills in the inspector', inspector.length === 0, JSON.stringify(inspector));
// 14f. The other direction: a control narrow enough to swallow its whole label draws nothing
// at all, which the overflow walk cannot see – an empty text spills over no edge.
for (const tab of ['scene', 'graph', 'texture', 'container', 'list', 'particles']) {
await showTab(tab);
const dropped = await json('__gm.labelsDropped()');
check(
`no button is left without its label in the ${tab} tab`,
dropped.length === 0,
JSON.stringify(dropped),
);
}
// The narrowest buttons in the editor sit on a behaviour's variable rows.
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'mover' })");
const onVariables = await json('__gm.labelsDropped()');
check(
'the variable rows keep their buttons labelled',
onVariables.length === 0,
JSON.stringify(onVariables),
);
}