Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners and the notes that carry a reason, a measured fact or an external contract. ButtonOptions.width claimed to be a fixed width but only set minWidth, so the Graph and Texture pickers grew with the name they showed. It is now what it promises, with a separate minWidth for the buttons that have two forms. Narrow buttons shrink their padding, otherwise a 24px icon button had 8px left for its glyph and truncate dropped it entirely. __gm.labelsDropped() guards that class: the overflow guard only saw text that was too wide, never a label truncated to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f16801aef3
commit
4d84231b9f
115 files changed
+707
-1564
No files matched your search
@@ -19,11 +19,9 @@ export async function assets(t) {
|
||||
check('+ menu creates a sprite asset', names.includes('Sprite'), JSON.stringify(names));
|
||||
await shot('12-new-asset');
|
||||
|
||||
// 12. Asset defaults: the Crate asset deviates in two properties – height 60 (schema default
|
||||
// 100) and its red colour (the sprite default is white, i.e. "no tint") – so the rows
|
||||
// carry two resets. Resetting height puts it back to 100 and removes *its* button while
|
||||
// the colour keeps one; undo brings 60 back. Rows run Enabled, Width, Height, Color,
|
||||
// Texture, so height's reset is the first and colour's the last.
|
||||
// 12. Asset defaults: the Crate deviates in height (60 against the schema's 100) and colour,
|
||||
// so two rows carry a reset. Rows run Enabled, Width, Height, Color, Texture, so height's
|
||||
// reset is the first on screen and colour's the last.
|
||||
await select({ kind: 'asset', assetId: 'crate' });
|
||||
await sleep(200);
|
||||
const heightReset = await evaluate(`__gm.locate('↺')`);
|
||||
|
||||
@@ -186,9 +186,8 @@ export async function graph(t) {
|
||||
await undo();
|
||||
check('undo removes the variable', (await variableKeys()) === 'speed', await variableKeys());
|
||||
|
||||
// 13h. The tab keeps its subject. A selection about something else (nothing, a camera) leaves
|
||||
// the graph alone – clicking a sprite to look at it must not empty the editor you are
|
||||
// working in – and the header picker switches behaviors without any selection at all.
|
||||
// 13h. The tab keeps its subject: selecting something else leaves the graph alone, and the
|
||||
// header picker switches behaviors without any selection at all.
|
||||
const size = await graphCounts();
|
||||
const readout = `${size.nodes} nodes · ${size.edges} links`;
|
||||
check(
|
||||
@@ -208,8 +207,7 @@ export async function graph(t) {
|
||||
'graph editor keeps its behavior for a non-behavior asset',
|
||||
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
||||
);
|
||||
// The picker is the *last* "Mover" on screen: the assets panel shows one too, and the
|
||||
// inspector is on the camera.
|
||||
// The picker is the *last* "Mover" on screen: the assets panel shows one too.
|
||||
const pickerAt = await locate('Mover', true);
|
||||
await click(pickerAt.x, pickerAt.y);
|
||||
await sleep(150);
|
||||
|
||||
+6
-12
@@ -38,23 +38,17 @@ export async function menus(t) {
|
||||
await shot('31-long-parent-menu');
|
||||
|
||||
// 18a. The entries scroll under the wheel; the search box above them does not move.
|
||||
//
|
||||
// Anchored on a filler row rather than the list's "None" entry: "None" is also what an empty
|
||||
// dropdown reads, so `locate('None', true)` could just as well find a *field* in the
|
||||
// inspector behind the menu – one that never moves, whatever the wheel does. "Filler 1"
|
||||
// exists only inside this list.
|
||||
// Anchored on a filler row: "None" is also what an empty dropdown reads, so locating it could
|
||||
// find an inspector field behind the menu instead – one that never moves.
|
||||
const firstBefore = await locate('Filler 1', true);
|
||||
const searchBefore = await locate('Search…', true);
|
||||
// Move onto the list before turning the wheel. A wheel event is delivered to whatever is
|
||||
// *hovered*, and the last thing the pointer touched was the field above the menu – without
|
||||
// this the scroll is routed at that field instead, which is what a user could never do.
|
||||
// Move onto the list first: a wheel event goes to whatever is hovered, and the pointer last
|
||||
// touched the field above the menu.
|
||||
await mouse('mouseMoved', rect.x + rect.w / 2, rect.y + rect.h / 2);
|
||||
await sleep(50);
|
||||
await mouse('mouseWheel', rect.x + rect.w / 2, rect.y + rect.h / 2, 'none', { deltaX: 0, deltaY: 240 });
|
||||
// Read twice, and wait properly. The scroll lands a frame or two later than one might
|
||||
// expect: measured here, the list had not moved 40 ms after the wheel and had moved at
|
||||
// 340 ms. A single early reading made this check fail in the full suite while passing on
|
||||
// its own — the two readings keep that visible instead of turning it back into a mystery.
|
||||
// Measured here: the list had not moved 40 ms after the wheel but had at 340 ms. Both
|
||||
// readings are reported so an early one cannot look like a mystery failure again.
|
||||
await sleep(40);
|
||||
const rightAfter = await locate('Filler 1', true);
|
||||
await sleep(300);
|
||||
|
||||
+10
-26
@@ -1,10 +1,6 @@
|
||||
/**
|
||||
* Uploading a picture and wearing it. Runs last because it adds an asset and repaints the
|
||||
* scene; the checks that count assets live in earlier steps.
|
||||
*
|
||||
* The image is built in the page and handed over as a paste, which is a real user gesture and
|
||||
* exercises the whole chain: decode → upload → the server storing it under its content hash →
|
||||
* the asset → the renderer. Nothing here is stubbed.
|
||||
*/
|
||||
|
||||
/** Paints a four-quadrant PNG in the page and pastes it. The quadrants make the mapping readable. */
|
||||
@@ -23,7 +19,6 @@ const PASTE_IMAGE = `(async () => {
|
||||
window.dispatchEvent(new ClipboardEvent('paste', { clipboardData: transfer, bubbles: true }));
|
||||
})()`;
|
||||
|
||||
/** How far apart two `#rrggbb` colours are, summed over the channels. */
|
||||
function colourDistance(a, b) {
|
||||
const channels = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||||
const [ar, ag, ab] = channels(a);
|
||||
@@ -34,9 +29,8 @@ function colourDistance(a, b) {
|
||||
export async function textures(t) {
|
||||
const { check, sleep, json, evaluate, shot, mutate, press } = t;
|
||||
|
||||
// The previous step leaves a picker open and a text field focused. Both matter here: paste is
|
||||
// deliberately ignored while an input has focus, so that Ctrl+V inside a field still edits
|
||||
// text. Closing and blurring is what clicking the canvas would do.
|
||||
// The previous step leaves a picker open and a text field focused, and paste is deliberately
|
||||
// ignored while an input has focus, so that Ctrl+V inside a field still edits text.
|
||||
await press('Escape');
|
||||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||||
await sleep(200);
|
||||
@@ -61,9 +55,8 @@ export async function textures(t) {
|
||||
JSON.stringify({ w: texture.w, h: texture.h }),
|
||||
);
|
||||
|
||||
// 19a. Importing the same picture again is a no-op with an explanation: the server hands back
|
||||
// the file it already has (the name is the hash), so the existing asset is reused rather
|
||||
// than a second one made for the same bytes.
|
||||
// 19a. Importing the same picture again reuses the asset: the server hands back the file it
|
||||
// already has, since the name is the content hash.
|
||||
await evaluate(PASTE_IMAGE);
|
||||
await sleep(2500);
|
||||
const again = await json(
|
||||
@@ -81,15 +74,9 @@ export async function textures(t) {
|
||||
);
|
||||
|
||||
// 19b. The picture is actually drawn – a sprite wearing it is no longer one flat colour.
|
||||
//
|
||||
// On a sprite of its own, deliberately: eleven steps of edits have been through the fixture's
|
||||
// objects by now (resized, re-parented, cloned forty times, covered by colliders and gizmos),
|
||||
// and a colour read off the screen must not depend on any of that. This one is white (so the
|
||||
// picture shows its own colours instead of a tint), large enough that a quarter-width sample
|
||||
// sits far from every quadrant border, and on top of everything else.
|
||||
// Both are clones of things that already exist rather than hand-written literals: a scene
|
||||
// object has fields a literal keeps forgetting (`editorOffset` was the one that bit here –
|
||||
// the position went NaN and the object was culled without an error).
|
||||
// On a fresh white sprite of its own, so the sample does not depend on eleven steps of edits
|
||||
// to the fixture. Cloned rather than written as a literal: a literal missed `editorOffset`,
|
||||
// which sent the position to NaN and culled the object without an error.
|
||||
await mutate(`
|
||||
const project = __gm.state.project;
|
||||
const sprite = structuredClone(project.assets.find((a) => a.type === 'sprite'));
|
||||
@@ -138,12 +125,9 @@ export async function textures(t) {
|
||||
);
|
||||
await shot('32-textured-sprite');
|
||||
|
||||
// 19b-i. Switching the Filter property takes effect at once, without a reload.
|
||||
//
|
||||
// Read off the screen rather than off `scaleMode`, because the property was always assigned
|
||||
// correctly – what used to survive the change was the GPU sampler behind it. The measurement
|
||||
// is what "smooth" means: across the red/green border, nearest sampling shows only the two
|
||||
// pure colours, linear sampling blends them into something that is neither.
|
||||
// 19b-i. Switching the Filter property takes effect at once, without a reload. Read off the
|
||||
// screen rather than off `scaleMode`: the property was always assigned correctly, it was the
|
||||
// GPU sampler behind it that used to survive the change.
|
||||
const enlarge = (w, h) => `
|
||||
const sprite = __gm.state.project.assets.find((a) => a.id === 'tex-sprite');
|
||||
sprite.props.width = ${w};
|
||||
|
||||
+13
-33
@@ -1,13 +1,10 @@
|
||||
/**
|
||||
* The Texture tab: cutting frames out of a sheet, and a sprite wearing one.
|
||||
*
|
||||
* Runs after the texture step, whose four-quadrant 64x48 picture is what makes this checkable
|
||||
* at all – a frame over one quadrant has a colour of its own, so "which part of the sheet is
|
||||
* on screen" is a question `pixelAt` can answer. Nothing here is stubbed: the frame is made by
|
||||
* dragging on the canvas, and the sprite is read back off the rendered pixels.
|
||||
* Runs after the texture step, whose four-quadrant 64x48 picture gives every quadrant a colour
|
||||
* of its own – that is what makes "which part of the sheet is on screen" a `pixelAt` question.
|
||||
*/
|
||||
|
||||
/** How far apart two `#rrggbb` colours are, summed over the channels. */
|
||||
function colourDistance(a, b) {
|
||||
const channels = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||||
const [ar, ag, ab] = channels(a);
|
||||
@@ -21,21 +18,12 @@ const PALETTE = {
|
||||
green: '#3cc85a',
|
||||
blue: '#466ef0',
|
||||
white: '#f5f5f5',
|
||||
/**
|
||||
* The sprite's own colour for the fallback check, deliberately a colour the sheet does not
|
||||
* contain. The sheet's fourth quadrant is near-white, so leaving the sprite white would make
|
||||
* "it fell back to its colour" and "it is showing that quadrant" the same measurement.
|
||||
*/
|
||||
plain: '#ff00ff',
|
||||
};
|
||||
|
||||
/**
|
||||
* Which of the known colours a sampled pixel is: the nearest one by channel distance.
|
||||
*
|
||||
* Asked this way rather than "is it exactly green" on purpose. The scene carries editor gizmos
|
||||
* by this point – a collider's fill is drawn over its body at 12 % alpha – so a sample can be
|
||||
* tinted by a few percent without anything being wrong. That shifts a colour slightly; it
|
||||
* cannot make one quadrant of the sheet read as another, which is the only question here.
|
||||
* Nearest known colour rather than an exact match: editor gizmos (a collider fill at 12 % alpha)
|
||||
* tint a sample by a few percent, which still cannot make one quadrant read as another.
|
||||
*/
|
||||
function nearestColour(sample) {
|
||||
let best = '';
|
||||
@@ -68,9 +56,8 @@ export async function frames(t) {
|
||||
);
|
||||
await shot('33-texture-tab');
|
||||
|
||||
// 21. Dragging on the picture cuts a frame. The drag is aimed at the top-right quadrant of
|
||||
// the sheet (32,0)-(64,24), inset by a pixel so the press cannot land on the edge of a
|
||||
// neighbouring frame and be read as a resize instead of a new rectangle.
|
||||
// 21. Dragging on the picture cuts a frame. Aimed at the top-right quadrant (32,0)-(64,24),
|
||||
// inset by a pixel: a press on a frame edge is read as a resize, not a new rectangle.
|
||||
const from = await json('__gm.texture.pointAt(33, 1)');
|
||||
const to = await json('__gm.texture.pointAt(63, 23)');
|
||||
check('the sheet is on screen', Boolean(from) && Boolean(to), JSON.stringify({ from, to }));
|
||||
@@ -111,11 +98,7 @@ export async function frames(t) {
|
||||
await t.redo();
|
||||
await sleep(400);
|
||||
|
||||
// 22. A sprite wearing the frame shows that part of the sheet and nothing else. This is the
|
||||
// check the whole feature exists for: the model difference between "whole picture" and
|
||||
// "this frame" is one id, so only the drawn pixels can tell them apart.
|
||||
// The sprite from the texture step is reused, white and unparented, but its texture
|
||||
// reference was left dangling there – it is pointed at the frame here.
|
||||
// 22. A sprite wearing the frame shows that part of the sheet and nothing else.
|
||||
await evaluate(`(() => {
|
||||
const sprite = __gm.state.project.assets.find((a) => a.id === 'tex-sprite');
|
||||
sprite.props.color = 0xffffff;
|
||||
@@ -132,9 +115,8 @@ export async function frames(t) {
|
||||
json(`__gm.pixelAt(${Math.round(box.x + fx * box.w)}, ${Math.round(box.y + fy * box.h)})`);
|
||||
const corners = [await at(-0.3, -0.3), await at(0.3, -0.3), await at(-0.3, 0.3), await at(0.3, 0.3)];
|
||||
const read = corners.map(nearestColour);
|
||||
// Without the frame these four corners read red / green / blue / white – that is what the
|
||||
// texture step asserts one screen earlier. Reading green four times is therefore the exact
|
||||
// statement that the fill was confined to one quadrant of the sheet.
|
||||
// Without the frame these corners read red / green / blue / white, as the texture step
|
||||
// asserts – so four greens is the statement that the fill stayed inside one quadrant.
|
||||
check(
|
||||
'the whole sprite shows the one quadrant the frame cuts out',
|
||||
read.every((c) => c === 'green'),
|
||||
@@ -143,10 +125,9 @@ export async function frames(t) {
|
||||
await shot('35-sprite-wearing-a-frame');
|
||||
}
|
||||
|
||||
// 23. Slicing offers a live count before it commits – the readout is what makes the numbers
|
||||
// answerable without undoing.
|
||||
// Repainted magenta before slicing: with the frame gone the sprite falls back to this, and
|
||||
// magenta is nowhere in the sheet, so the fallback cannot be confused with a part of it.
|
||||
// 23. Slicing offers a live count before it commits.
|
||||
// Repainted magenta first: magenta is nowhere in the sheet, so the fallback check below
|
||||
// cannot be confused with a part of the picture.
|
||||
await evaluate(`(() => {
|
||||
__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.color = 0xff00ff;
|
||||
__gm.state.preview();
|
||||
@@ -180,8 +161,7 @@ export async function frames(t) {
|
||||
check('and numbers them in reading order', sliced[0]?.startsWith('Frame 1 '), JSON.stringify(sliced[0]));
|
||||
await shot('36-sliced-sheet');
|
||||
|
||||
// 23a. The sprite pointed at the frame that slicing removed falls back to its colour rather
|
||||
// than to a wrong part of the picture.
|
||||
// 23a. A sprite pointed at a frame slicing removed falls back to its colour.
|
||||
await evaluate("__gm.showTab('scene')");
|
||||
await sleep(700);
|
||||
if (box) {
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
* 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.
|
||||
*
|
||||
* `__gm.textOverflows()` is a general guard rather than a check on one widget: it walks every
|
||||
* visible text and reports the ones drawn wider than the widget that owns them. It was verified
|
||||
* to actually catch this class of bug – against a build without the fix it reported the graph
|
||||
* picker at 106 px over, and nothing else.
|
||||
* `__gm.textOverflows()` walks every visible text and reports the ones drawn wider than their
|
||||
* widget; against a build without the fix it reported the graph picker at 106 px over.
|
||||
*/
|
||||
|
||||
/** A name far wider than any 200 px picker, so a control that does not truncate must spill. */
|
||||
@@ -14,6 +12,17 @@ const LONG_NAME = 'Player movement with jumping and coyote time';
|
||||
export async function overflow(t) {
|
||||
const { check, sleep, json, evaluate, shot, mutate } = t;
|
||||
|
||||
const pickerWidths = async () => {
|
||||
const out = {};
|
||||
for (const tab of ['graph', 'texture']) {
|
||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||||
await sleep(300);
|
||||
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.name = ${JSON.stringify(LONG_NAME)};
|
||||
@@ -21,8 +30,18 @@ export async function overflow(t) {
|
||||
__gm.state.select(null)`);
|
||||
await sleep(600);
|
||||
|
||||
// 21. Every tab, because each one owns a different set of controls: the two header bars with
|
||||
// their pickers, the inspector's dropdowns, and the toolbar.
|
||||
// 21. The pickers keep the width they were given. `width` used to set only a minimum, so a
|
||||
// picker grew with whatever name it happened to show and the header reflowed on load.
|
||||
const after = await pickerWidths();
|
||||
for (const tab of ['graph', 'texture']) {
|
||||
check(
|
||||
`the ${tab} picker keeps its width when the name gets long`,
|
||||
before[tab] === 200 && after[tab] === 200,
|
||||
`before ${before[tab]}, after ${after[tab]}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 21a. Every tab, because each one owns a different set of controls.
|
||||
for (const tab of ['scene', 'graph', 'texture', 'game']) {
|
||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||||
await sleep(400);
|
||||
@@ -34,7 +53,7 @@ export async function overflow(t) {
|
||||
);
|
||||
}
|
||||
|
||||
// 21a. Shortened, not simply dropped: the value must still be readable and marked as cut.
|
||||
// 21b. Shortened, not simply dropped: the value must still be readable and marked as cut.
|
||||
await evaluate("__gm.showTab('graph')");
|
||||
await sleep(400);
|
||||
const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement'));
|
||||
@@ -45,7 +64,7 @@ export async function overflow(t) {
|
||||
);
|
||||
await shot('36-long-labels');
|
||||
|
||||
// 21b. An inspector full of dropdowns – the parent row and the enum fields – with a selection
|
||||
// 21c. An inspector full of dropdowns – the parent row and the enum fields – with a selection
|
||||
// whose asset name is now overlong too.
|
||||
await evaluate("__gm.showTab('scene')");
|
||||
await sleep(300);
|
||||
@@ -53,4 +72,26 @@ export async function overflow(t) {
|
||||
await sleep(500);
|
||||
const inspector = await json('__gm.textOverflows()');
|
||||
check('no label spills in the inspector', inspector.length === 0, JSON.stringify(inspector));
|
||||
|
||||
// 21d. 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']) {
|
||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||||
await sleep(400);
|
||||
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' })");
|
||||
await sleep(400);
|
||||
const onVariables = await json('__gm.labelsDropped()');
|
||||
check(
|
||||
'the variable rows keep their buttons labelled',
|
||||
onVariables.length === 0,
|
||||
JSON.stringify(onVariables),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user