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); }