Files
feuerwerk/tools/e2e/15-aspect.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

157 lines
6.9 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.
/**
* The ↔ / ↕ buttons beside a sprite's Width and Height (absent without a picture). Runs on the imported
* 64 × 48 sheet plus a frame of a different shape, so "sheet or frame?" shows in the numbers.
*/
/** The rows are read as numbers, so a float from a division needs a tolerance. */
const near = (value, expected) => Math.abs(value - expected) < 0.01;
const spriteProp = (key) => `__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.${key}`;
export async function aspect(t) {
const { check, sleep, json, evaluate, shot, clickText, press, undo, mutate, select } = t;
/** Whether the size button of a row is there at all, in either its plain or its menu form. */
const sizeButton = async (glyph) =>
(await json(`__gm.locate('${glyph}')`))
? 'plain'
: (await json(`__gm.locate('${glyph} ▾')`))
? 'menu'
: '';
await press('Escape');
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
const sheet = await json("(__gm.state.project.assets.find((a) => a.type === 'texture') ?? {}).id ?? ''");
check('the imported sheet is still there', sheet !== '', sheet);
// 15a. A sprite wearing nothing has nothing to be proportional to, so the buttons are absent.
// Edited as an asset: one base value per row instead of an override, so the numbers below
// say what they mean.
await mutate(`
const props = __gm.state.project.assets.find((a) => a.id === 'tex-sprite').props;
props.texture = '';
props.width = 100;
props.height = 100`);
await select({ kind: 'asset', assetId: 'tex-sprite' });
await sleep(600);
check('the sprite asset is in the inspector', Boolean(await json("__gm.locate('Width')")));
check('no texture, no aspect buttons', (await sizeButton('↔')) === '' && (await sizeButton('↕')) === '');
await shot('40-aspect-without-a-texture');
// 15b. Wearing the 64 x 48 sheet, a 100 x 100 sprite is neither proportional nor at pixel size,
// so both rows offer two things and show the menu form.
await mutate(`${spriteProp('texture')} = ${JSON.stringify(sheet)}`);
await sleep(500);
check(
'a texture brings both buttons out, offering fit and pixel size',
(await sizeButton('↔')) === 'menu' && (await sizeButton('↕')) === 'menu',
);
await shot('41-aspect-buttons');
// 15c. Fit moves the width only, to the sheet's ratio: 100 x 64/48.
await clickText('↔ ▾');
await clickText('Fit to 64 × 48');
await sleep(500);
const fitted = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
check(
'↔ sets the width from the height and the sheet ratio',
near(fitted.w, (100 * 64) / 48) && fitted.h === 100,
JSON.stringify(fitted),
);
check(
'and the fit drops out of the menu once the sprite is proportional',
(await sizeButton('↔')) === 'plain' && (await sizeButton('↕')) === 'plain',
);
await shot('42-aspect-fitted');
// 15d. One command, so it is one undo step – and the menu form comes back with the old shape.
await undo();
await sleep(500);
check('undo takes the width back', (await json(spriteProp('width'))) === 100);
check('and the buttons return with it', (await sizeButton('↔')) === 'menu');
await t.redo();
await sleep(500);
// 15e. What is left over is the pixel size, now a direct button: it writes *both* sides, and
// the whole button goes because there is nothing left to offer.
await clickText('↔');
await sleep(500);
const pixels = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
check(
'the pixel-size action sets both sides to the picture',
pixels.w === 64 && pixels.h === 48,
JSON.stringify(pixels),
);
check(
'and nothing is left to offer, so no button is drawn',
(await sizeButton('↔')) === '' && (await sizeButton('↕')) === '',
);
await undo();
await sleep(500);
check(
'both sides come back in one undo step',
near(await json(spriteProp('width')), (100 * 64) / 48) && (await json(spriteProp('height'))) === 100,
);
await t.redo();
await sleep(500);
// 15f. A frame is the picture the sprite wears, not the sheet it was cut from: 40 x 10 against
// the sheet's 64 x 48, so the two ratios cannot be confused for one another.
await mutate(`
const asset = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(sheet)});
asset.frames.push({ id: 'aspect-frame', name: 'Wide', x: 0, y: 0, w: 40, h: 10 });
${spriteProp('texture')} = 'aspect-frame'`);
await sleep(500);
check('a frame of another shape brings the buttons back', (await sizeButton('↕')) === 'menu');
const before = await json(spriteProp('width'));
await clickText('↕ ▾');
await clickText('Fit to 40 × 10');
await sleep(500);
const framed = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
check(
'↕ sets the height from the frame ratio, not the sheet it belongs to',
near(framed.h, (before * 10) / 40) && framed.w === before,
JSON.stringify({ framed, sheetWouldGive: (before * 48) / 64 }),
);
check('and only the pixel size is left to offer', (await sizeButton('↕')) === 'plain');
// 15g. The same rows on a placed object write an override rather than the asset value.
await mutate(`${spriteProp('texture')} = ${JSON.stringify(sheet)}`);
await select({ kind: 'object', holderId: 'scene1', objectIds: ['otex'], objectId: 'otex' });
await sleep(600);
await mutate("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides.height = 90");
check('the object row offers the fit as well', (await sizeButton('↔')) === 'menu');
// The crowded case: an overridden, out-of-proportion row carries ↔ ▾ next to ↑ and ↺.
check('and shows it beside the override buttons', (await json('__gm.textOverflows()')).length === 0);
await shot('43-aspect-beside-the-override-buttons');
await clickText('↔ ▾');
await clickText('Fit to 64 × 48');
await sleep(500);
const override = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
check(
'fitting a scene object writes an override, leaving the asset alone',
near(override.width, (90 * 64) / 48) && near(await json(spriteProp('width')), framed.w),
JSON.stringify({ override, asset: await json(spriteProp('width')) }),
);
await shot('44-aspect-on-an-object');
// 15h. The pixel size on an object writes both overrides, and as one undo step.
await clickText('↔');
await sleep(500);
const sized = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
check(
'the pixel size writes both overrides at once',
sized.width === 64 && sized.height === 48,
JSON.stringify(sized),
);
await undo();
await sleep(500);
const back = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
check(
'and one undo takes both back',
near(back.width, (90 * 64) / 48) && back.height === 90,
JSON.stringify(back),
);
}