The suite job for 0d20776 died in module 06. The new font and theme
moved the layout, and three checks relied on the old one.
- 06: the ranking step right-clicked the spot where step 6d had just
dropped a Log node. Whether that click hits the node depends on how
the layout snaps it. It now asks for a fresh empty spot.
- 14 and 27 expected a shut panel to keep 30 px. The header is
Theme.rowHeight + 4, now 26. A new hook __gm.panelHeader hands the
checks the real height, so the next theme change does not break
them again.
Tests:
- ./check.sh: 141 files, 2080 tests, exit 0
- Local full run before the fix: 06 passed with the new spot, only the
three header checks in 14 and 27 failed.
- GM_STEPS=1-14,27 after the fix: ALL PASSED, 457 checks
- No new checks, the total stays at 1286.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
7.6 KiB
JavaScript
180 lines
7.6 KiB
JavaScript
import { edge, node } from '../lib/builders.mjs';
|
||
import { LANES, scaffoldBehavior } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* The console panel: every `console.*` call lands in it with its level, a Log node's message
|
||
* arrives through the very same path, the play status line deliberately does not, and the
|
||
* divider above it resizes down to the bare header. The step removes everything it made.
|
||
*/
|
||
|
||
const SPOKEN = 'the crate says hello';
|
||
|
||
/** `On start → Log`: the one path a project itself writes a line on. */
|
||
const talkerGraph = {
|
||
nodes: [node('c-start', 'OnStart', 0, 0), node('c-log', 'Log', 340, 0, { message: SPOKEN })],
|
||
edges: [edge('c-e1', 'c-start', 'exec', 'c-log', 'exec')],
|
||
};
|
||
|
||
export async function consolePanel(t) {
|
||
const { check, sleep, json, evaluate, shot, drag, wheel, clickText, play, stop } = t;
|
||
const lines = () => json('__gm.console.lines()');
|
||
const texts = async () => (await lines()).map((l) => l.text);
|
||
const height = () => json('__gm.console.height()');
|
||
const colorOf = (text) => json(`__gm.textColor(${JSON.stringify(text)})`);
|
||
/** The panel rebuilds on the frame after a push, so every read waits one out. */
|
||
const settle = () => sleep(300);
|
||
|
||
await evaluate('__gm.console.clear()');
|
||
await settle();
|
||
|
||
// 27a. The captured path: each console method with its own level, and the level drawn as colour.
|
||
await evaluate(`console.log('plain line 27'); console.warn('warned 27'); console.error('failed 27')`);
|
||
await settle();
|
||
const captured = await lines();
|
||
check(
|
||
'every console level lands in the panel, newest last',
|
||
JSON.stringify(captured.map((l) => ({ level: l.level, text: l.text }))) ===
|
||
JSON.stringify([
|
||
{ level: 'info', text: 'plain line 27' },
|
||
{ level: 'warn', text: 'warned 27' },
|
||
{ level: 'error', text: 'failed 27' },
|
||
]),
|
||
captured,
|
||
);
|
||
const [info, warn, error] = await Promise.all([
|
||
colorOf('plain line 27'),
|
||
colorOf('warned 27'),
|
||
colorOf('failed 27'),
|
||
]);
|
||
check(
|
||
'a warning is drawn amber and an error red, a plain line in the text colour',
|
||
info === '#e6e8ee' && warn === '#f2c84b' && error === '#e0553f',
|
||
{ info, warn, error },
|
||
);
|
||
|
||
// 27a2. Every line carries the clock time of its call, drawn muted in its own column left of
|
||
// the message – so the message itself is still exactly what was logged.
|
||
const stamps = captured.map((l) => l.at);
|
||
const stampAt = await json(`__gm.locate(${JSON.stringify(captured[0].at)})`);
|
||
const messageAt = await json(`__gm.locate('plain line 27')`);
|
||
check(
|
||
'each line is stamped HH:MM:SS, drawn muted and left of the message',
|
||
stamps.every((at) => /^\d{2}:\d{2}:\d{2}$/.test(at)) &&
|
||
stampAt.x < messageAt.x &&
|
||
(await colorOf(captured[0].at)) === '#8a90a0',
|
||
JSON.stringify({ stamps, stampAt, messageAt }),
|
||
);
|
||
|
||
// 27b. Several arguments become one line, and an Error shows its message instead of [object].
|
||
await evaluate(`console.log('joined 27', 3, { a: 1 }); console.error(new Error('thrown 27'))`);
|
||
await settle();
|
||
const formatted = (await texts()).slice(-2);
|
||
check(
|
||
'arguments are joined and an Error reads as its message',
|
||
formatted[0] === 'joined 27 3 {"a":1}' && formatted[1]?.includes('thrown 27') === true,
|
||
formatted,
|
||
);
|
||
|
||
// 27b-ii. A group keeps its head and indents what follows: Pixi's deprecation warnings put the
|
||
// message into `groupCollapsed` with `%c` styles and only the stack into `warn`, so without this
|
||
// the panel showed a stack with no head.
|
||
await evaluate(
|
||
`console.groupCollapsed('%cgrouped 27: %c%s', 'color:red', 'color:blue', 'head'); console.warn(' at frame 27'); console.groupEnd(); console.log('after 27')`,
|
||
);
|
||
await settle();
|
||
const grouped = (await texts()).slice(-3);
|
||
check(
|
||
'a group head reads without its %c styles and the lines under it are indented',
|
||
grouped[0] === 'grouped 27: head' && grouped[1] === ' at frame 27' && grouped[2] === 'after 27',
|
||
grouped,
|
||
);
|
||
|
||
// 27c. The real path a project uses: a Log node during play writes through `console.log`.
|
||
const rig = await scaffoldBehavior(t, {
|
||
assetId: 'talker',
|
||
name: 'Talker',
|
||
graph: talkerGraph,
|
||
boxId: 'c-box',
|
||
boxName: 'Talker box',
|
||
logicId: 'c-logic',
|
||
y: LANES.console,
|
||
});
|
||
await evaluate('__gm.console.clear()');
|
||
await play();
|
||
await settle();
|
||
const played = await texts();
|
||
check(
|
||
'a Log node reaches the panel with the behavior it came from',
|
||
played.some((line) => line === `[behavior Talker] ${SPOKEN}`),
|
||
played,
|
||
);
|
||
// 27d. The status line is a mode, not an event: the toolbar says it, the console stays out of it.
|
||
const notice = await json('__gm.state.notice');
|
||
check(
|
||
'the play status line shows in the toolbar but is kept out of the console',
|
||
notice.startsWith('Playing') && !played.some((line) => line.startsWith('Playing')),
|
||
{ notice, played },
|
||
);
|
||
await stop();
|
||
await settle();
|
||
await shot('27-console-after-play');
|
||
|
||
// 27e. Clear empties it, and only it – the toolbar notice is not the console's to touch.
|
||
await clickText('Clear');
|
||
await settle();
|
||
check('the Clear button empties the panel', (await lines()).length === 0);
|
||
|
||
// 27f. The ring buffer keeps the tail, not the head.
|
||
await evaluate(`for (let i = 0; i < 520; i += 1) console.log('ring ' + i)`);
|
||
await sleep(800);
|
||
const ring = await texts();
|
||
check(
|
||
'past its capacity the panel keeps the newest 500 lines',
|
||
ring.length === 500 && ring[0] === 'ring 20' && ring.at(-1) === 'ring 519',
|
||
{ length: ring.length, first: ring[0], last: ring.at(-1) },
|
||
);
|
||
const tail = await json('__gm.console.scroll()');
|
||
check('it follows the tail, so the newest line is the one showing', tail.offset === tail.max, tail);
|
||
|
||
// 27g. Reading back means the tail must stop pulling, or the line under the pointer runs away.
|
||
// A full buffer would hide that: dropping a line as one arrives leaves the height unchanged.
|
||
await evaluate('__gm.console.clear()');
|
||
await settle();
|
||
await evaluate(`for (let i = 0; i < 60; i += 1) console.log('scroll ' + i)`);
|
||
await settle();
|
||
const panel = await json('__gm.console.centre()');
|
||
await wheel(panel.x, panel.y, -200);
|
||
await settle();
|
||
const scrolledUp = await json('__gm.console.scroll()');
|
||
await evaluate(`console.log('arrived while reading 27')`);
|
||
await settle();
|
||
const stayed = await json('__gm.console.scroll()');
|
||
check(
|
||
'a line arriving while you read further up does not yank the view down',
|
||
scrolledUp.offset === scrolledUp.max - 200 &&
|
||
stayed.max > scrolledUp.max &&
|
||
stayed.offset === scrolledUp.offset,
|
||
JSON.stringify({ scrolledUp, stayed }),
|
||
);
|
||
await evaluate('__gm.console.clear()');
|
||
await settle();
|
||
|
||
// 27h. The divider resizes the panel, and shutting it leaves the header to grab it by again.
|
||
const start = await height();
|
||
const bar = await json('__gm.console.splitterAt()');
|
||
await drag(bar.x, bar.y, bar.x, bar.y - 60, { steps: 6 });
|
||
const taller = await height();
|
||
check('dragging the divider up makes the console taller', taller === start + 60, { start, taller });
|
||
const bar2 = await json('__gm.console.splitterAt()');
|
||
await drag(bar2.x, bar2.y, bar2.x, bar2.y + 400, { steps: 10 });
|
||
const shut = await height();
|
||
const header = await json('__gm.panelHeader');
|
||
check('dragged shut it stops at its own header instead of vanishing', shut === header, { shut, header });
|
||
const bar3 = await json('__gm.console.splitterAt()');
|
||
await drag(bar3.x, bar3.y, bar3.x, bar3.y - (start - header), { steps: 8 });
|
||
check('and it comes back to the height it started at', (await height()) === start, await height());
|
||
|
||
const left = await rig.remove();
|
||
check('the step leaves nothing behind', left.objects === 0 && left.assets === 0, left);
|
||
}
|