Files
feuerwerk/tools/e2e/15-aspect.mjs
T
rootandClaude Fable 5.1 6471671f7d Refactor round: ship web/index.html, drop the remote browser, fix stale comments and README
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
  a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
  the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
  history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
  nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
  step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
  and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
  and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
  tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.

Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 15:44:05 +02:00

161 lines
7.0 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 sleep(400);
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 sleep(400);
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");
await sleep(400);
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 sleep(400);
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),
);
}