115 lines
4.5 KiB
JavaScript
115 lines
4.5 KiB
JavaScript
import { openTab } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* Render on change: the editor draws what changed, not sixty pictures a second.
|
||
*
|
||
* This is the one thing the rest of the suite cannot see. `pixelAt` re-renders the stage on
|
||
* demand, so every colour check would pass just as well on a screen that froze a minute ago –
|
||
* only the drawn-frame counter tells the difference between "drawn" and "merely correct".
|
||
*
|
||
* The step runs last and leaves the editor as it found it.
|
||
*/
|
||
export async function renderOnChange(t) {
|
||
const { check, sleep, evaluate, json, select, click, play, stop, shot, waitUntil } = t;
|
||
|
||
const stats = () => evaluate('__gm.renderStats()');
|
||
/** Frames drawn over `ms` of doing nothing at all. */
|
||
const drawnDuring = async (ms) => {
|
||
const before = await stats();
|
||
await sleep(ms);
|
||
const after = await stats();
|
||
return after.drawn - before.drawn;
|
||
};
|
||
/**
|
||
* Waits for the trailing window of the last event to run out. Measuring without this counts the
|
||
* frames the previous click is still entitled to and calls them a leak.
|
||
*
|
||
* The autosave is waited out first. It fires ~1 s after the last model change and paints the
|
||
* status twice – `saving`, then `saved` – so an earlier step's last edit lands in this step's
|
||
* quiet window and looks like a renderer that will not go to sleep. Those frames are earned:
|
||
* the status text really did change. The step has to stop measuring before them, not call them
|
||
* a leak.
|
||
*/
|
||
const settle = async (timeoutMs = 5000) => {
|
||
await waitUntil("!['dirty', 'saving'].includes(__gm.state.saveStatus)", {
|
||
timeout: timeoutMs,
|
||
label: 'the autosave finished',
|
||
});
|
||
for (let waited = 0; waited < timeoutMs; waited += 100) {
|
||
if (!(await stats()).awake) return true;
|
||
await sleep(100);
|
||
}
|
||
return false;
|
||
};
|
||
|
||
// 33a. A tab whose picture stands still: the scene viewport may wear a Time shader, and the
|
||
// particles and animation previews are moving pictures by definition.
|
||
await openTab(t, 'graph');
|
||
await select(null);
|
||
|
||
const quiet = await settle();
|
||
const idle = await drawnDuring(700);
|
||
check(
|
||
'an editor nobody touches draws no frames at all',
|
||
quiet && idle === 0,
|
||
`settled=${quiet}, ${idle} frames drawn`,
|
||
);
|
||
|
||
// 33b. A model change that arrives without any input – no click, no key – still reaches the
|
||
// screen. This is the wake-up that a strict dirty flag on the widgets would have missed.
|
||
const beforeChange = (await stats()).drawn;
|
||
await evaluate(`__gm.state.setNotice('render check', 'info')`);
|
||
await sleep(150);
|
||
const afterChange = (await stats()).drawn;
|
||
check(
|
||
'a change nobody clicked for is drawn anyway',
|
||
afterChange > beforeChange,
|
||
`${beforeChange} → ${afterChange}`,
|
||
);
|
||
|
||
// 33c. Input wakes it, and it goes back to sleep on its own afterwards.
|
||
await settle();
|
||
const beforeClick = (await stats()).drawn;
|
||
// A behavior's tab – its kind is an icon since #176, so found through the row, not by a word.
|
||
const shown = (await json('__gm.tabRow()')).shown;
|
||
const tab = shown.find((p) => p.kind === 'Behavior') ?? shown.find((p) => p.tab === 'scene');
|
||
await click(tab.x + 12, tab.y + tab.h / 2);
|
||
const afterClick = (await stats()).drawn;
|
||
check('a click draws again', afterClick > beforeClick, `${beforeClick} → ${afterClick}`);
|
||
|
||
const settled = await settle();
|
||
const backToSleep = await drawnDuring(700);
|
||
check(
|
||
'and it stops on its own once the click is dealt with',
|
||
settled && backToSleep === 0,
|
||
`settled=${settled}, ${backToSleep} frames`,
|
||
);
|
||
|
||
// 33d. While the game runs, the frame cannot come from an event: the world moves by itself.
|
||
await play();
|
||
const playing = await drawnDuring(600);
|
||
const fps = await evaluate('__gm.fps()');
|
||
check(
|
||
'a running game keeps getting frames without being touched',
|
||
playing > 10,
|
||
`${playing} frames in 600 ms`,
|
||
);
|
||
check('and the fps readout keeps measuring them', typeof fps === 'number' && fps > 0, `fps=${fps}`);
|
||
await shot('01-playing');
|
||
await stop();
|
||
|
||
// 33e. Stopping ends the reason to draw. Back off the scene tab first: a Time shader left on
|
||
// screen by an earlier step is a legitimate reason to keep drawing, and would mask this check.
|
||
await openTab(t, 'graph');
|
||
const stopped = await settle();
|
||
const afterStop = await drawnDuring(700);
|
||
check(
|
||
'stopping puts it back to sleep',
|
||
stopped && afterStop === 0,
|
||
`settled=${stopped}, ${afterStop} frames`,
|
||
);
|
||
|
||
await evaluate(`__gm.state.setNotice('', 'info')`);
|
||
await sleep(200);
|
||
}
|