Files
feuerwerk/tools/e2e/27-console.mjs
T
rootandClaude Fable 5.1 0e5e5ef925
game-maker / suite (push) Successful in 8m24s
Shaders draw in the sprite's own pass: a Mesh with the graph's fragment, no filter (#77)
A sprite's picture is now a Mesh (unit quad, sibling of its Graphics node, which keeps
only the hit area); a worn shader replaces Pixi's fragment in that draw, so there is no
intermediate texture and no extra render pass. Measured on the HD 630: 400 shaded
sprites hold 60 fps (was 34), 800 hold 56 (was 20).

Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader,
so GraphShader.wear hands it over; and MeshPipe caches whether a mesh batches and
re-checks it only in a frame without structure changes, so switching a shader on or
off calls mesh.unload().

The console panel mirrors console.group/groupCollapsed/groupEnd and fills the %s %d
%o %c directives, so Pixi's deprecation warning shows its head, not only its stack.
E2E 12 guards against the addChild-on-Graphics warning, 28 counts shaders instead of
filters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 15:31:08 +02:00

179 lines
7.6 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 { 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();
check('dragged shut it stops at its own header instead of vanishing', shut === 30, shut);
const bar3 = await json('__gm.console.splitterAt()');
await drag(bar3.x, bar3.y, bar3.x, bar3.y - (start - 30), { 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);
}