A test that waits for a deadline somebody guessed is waiting for the wrong thing, so it waits for the editor (#93)
game-maker / suite (push) Successful in 6m48s
game-maker / suite (push) Successful in 6m48s
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.
The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.
Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.
564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.
Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
This commit is contained in:
1 parent
c990dbcb26
commit
7e5003b590
41 files changed
+362
-732
No files matched your search
+17
-1
@@ -4,7 +4,17 @@ import { resolveObjects } from '../core/model/instance';
|
|||||||
import { findObjectHolder } from '../core/model/Project';
|
import { findObjectHolder } from '../core/model/Project';
|
||||||
import type { FrameRect } from '../core/model/texture';
|
import type { FrameRect } from '../core/model/texture';
|
||||||
import { effectiveScale, gameClock } from '../core/time';
|
import { effectiveScale, gameClock } from '../core/time';
|
||||||
import { activeDrag, Button, currentMenu, isRendering, renderCount, requestRender, ui, Widget } from '../ui';
|
import {
|
||||||
|
activeDrag,
|
||||||
|
afterFrames,
|
||||||
|
Button,
|
||||||
|
currentMenu,
|
||||||
|
isRendering,
|
||||||
|
renderCount,
|
||||||
|
requestRender,
|
||||||
|
ui,
|
||||||
|
Widget,
|
||||||
|
} from '../ui';
|
||||||
import { particleLibrary } from '../viewport/ParticleLibrary';
|
import { particleLibrary } from '../viewport/ParticleLibrary';
|
||||||
import { shaderLibrary } from '../viewport/ShaderLibrary';
|
import { shaderLibrary } from '../viewport/ShaderLibrary';
|
||||||
import { soundLibrary } from '../viewport/SoundLibrary';
|
import { soundLibrary } from '../viewport/SoundLibrary';
|
||||||
@@ -530,6 +540,12 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
|||||||
renderStats: () => ({ drawn: renderCount(), awake: isRendering() }),
|
renderStats: () => ({ drawn: renderCount(), awake: isRendering() }),
|
||||||
/** Draw again, whatever happens next – what a screenshot needs before it is taken. */
|
/** Draw again, whatever happens next – what a screenshot needs before it is taken. */
|
||||||
wake: () => requestRender(),
|
wake: () => requestRender(),
|
||||||
|
/**
|
||||||
|
* Resolves once the editor has drawn `count` more frames. The checks await this instead of
|
||||||
|
* sleeping a guessed number of milliseconds: a frame is the editor saying it has caught up,
|
||||||
|
* where a deadline is only the harness hoping it has.
|
||||||
|
*/
|
||||||
|
afterFrames: (count = 2, timeoutMs = 2000) => afterFrames(count, timeoutMs),
|
||||||
/** Visible nodes carrying a filter – one extra render pass each; since #77 a shader is none. */
|
/** Visible nodes carrying a filter – one extra render pass each; since #77 a shader is none. */
|
||||||
filterCount: () => {
|
filterCount: () => {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
|
|||||||
+8
-1
@@ -2,7 +2,14 @@ export { Widget, Box, type Size } from './Widget';
|
|||||||
export { HStack, VStack, Spacer, Stack, type StackOptions } from './layout/Stack';
|
export { HStack, VStack, Spacer, Stack, type StackOptions } from './layout/Stack';
|
||||||
export { Theme, textStyle, type TextStyleToken } from './Theme';
|
export { Theme, textStyle, type TextStyleToken } from './Theme';
|
||||||
export { initUI, ui, type UIContext } from './UIContext';
|
export { initUI, ui, type UIContext } from './UIContext';
|
||||||
export { initRenderLoop, isRendering, renderCount, requestRender, resetRenderLoop } from './render';
|
export {
|
||||||
|
afterFrames,
|
||||||
|
initRenderLoop,
|
||||||
|
isRendering,
|
||||||
|
renderCount,
|
||||||
|
requestRender,
|
||||||
|
resetRenderLoop,
|
||||||
|
} from './render';
|
||||||
export { drawBand, drawControlBox, drawGrid, type ControlBoxOptions, type GridOptions } from './draw';
|
export { drawBand, drawControlBox, drawGrid, type ControlBoxOptions, type GridOptions } from './draw';
|
||||||
export { clamp, centreY, formatBytes, formatNumber, NUMBER_DECIMALS, snapToStep, zoomFactor } from './math';
|
export { clamp, centreY, formatBytes, formatNumber, NUMBER_DECIMALS, snapToStep, zoomFactor } from './math';
|
||||||
export { activeDrag, type DragPayload, endDrag, onDragChange, startDrag } from './drag';
|
export { activeDrag, type DragPayload, endDrag, onDragChange, startDrag } from './drag';
|
||||||
|
|||||||
@@ -37,6 +37,39 @@ export function renderCount(): number {
|
|||||||
return rendered;
|
return rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolved once the next frame has been drawn; see `afterFrames`. */
|
||||||
|
let frameWaiters: Array<() => void> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves once `count` frames have actually been drawn – "the editor has caught up with what I
|
||||||
|
* just did", said as a fact instead of as a deadline. The checks wait on this rather than on a
|
||||||
|
* guessed number of milliseconds, which is both faster (a frame is ~16 ms) and truer.
|
||||||
|
*
|
||||||
|
* It wakes the loop itself, because an idle editor draws nothing and a promise waiting for a
|
||||||
|
* frame that nobody asked for never settles. `timeoutMs` is the other half of that promise: a
|
||||||
|
* wedged renderer must fail the check it belongs to, not hang the whole run.
|
||||||
|
*/
|
||||||
|
export function afterFrames(count = 1, timeoutMs = 2000): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let left = Math.max(1, count);
|
||||||
|
let done = false;
|
||||||
|
const finish = (): void => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const timer = setTimeout(finish, timeoutMs);
|
||||||
|
const tick = (): void => {
|
||||||
|
if (done) return;
|
||||||
|
if (--left <= 0) finish();
|
||||||
|
else frameWaiters.push(tick);
|
||||||
|
};
|
||||||
|
frameWaiters.push(tick);
|
||||||
|
requestRender();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes the renderer off the ticker and drives it from `requestRender()` instead. Added at the
|
* Takes the renderer off the ticker and drives it from `requestRender()` instead. Added at the
|
||||||
* ticker's lowest priority, so everything that may request a frame has run for this tick already.
|
* ticker's lowest priority, so everything that may request a frame has run for this tick already.
|
||||||
@@ -49,6 +82,10 @@ export function initRenderLoop(app: Application): void {
|
|||||||
if (!isRendering()) return;
|
if (!isRendering()) return;
|
||||||
rendered += 1;
|
rendered += 1;
|
||||||
app.render();
|
app.render();
|
||||||
|
// Taken first: a waiter that queues itself again must land in the next round, not this one.
|
||||||
|
const waiters = frameWaiters;
|
||||||
|
frameWaiters = [];
|
||||||
|
for (const w of waiters) w();
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
UPDATE_PRIORITY.LOW,
|
UPDATE_PRIORITY.LOW,
|
||||||
@@ -60,4 +97,5 @@ export function initRenderLoop(app: Application): void {
|
|||||||
export function resetRenderLoop(): void {
|
export function resetRenderLoop(): void {
|
||||||
deadline = 0;
|
deadline = 0;
|
||||||
rendered = 0;
|
rendered = 0;
|
||||||
|
frameWaiters = [];
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Application } from 'pixi.js';
|
import type { Application } from 'pixi.js';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
afterFrames,
|
||||||
initRenderLoop,
|
initRenderLoop,
|
||||||
isRendering,
|
isRendering,
|
||||||
renderCount,
|
renderCount,
|
||||||
@@ -93,4 +94,48 @@ describe('render loop', () => {
|
|||||||
tick();
|
tick();
|
||||||
expect(app.drawn).toBe(1);
|
expect(app.drawn).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('afterFrames resolves only once that many frames were really drawn', async () => {
|
||||||
|
const { app, tick } = fakeApp();
|
||||||
|
initRenderLoop(app as unknown as Application);
|
||||||
|
now += 500;
|
||||||
|
|
||||||
|
let done = false;
|
||||||
|
const waited = afterFrames(2).then(() => (done = true));
|
||||||
|
|
||||||
|
tick();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(done).toBe(false);
|
||||||
|
tick();
|
||||||
|
await waited;
|
||||||
|
expect(done).toBe(true);
|
||||||
|
expect(app.drawn).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('afterFrames wakes the loop itself, so an idle editor still answers', async () => {
|
||||||
|
const { app, tick } = fakeApp();
|
||||||
|
initRenderLoop(app as unknown as Application);
|
||||||
|
now += 500;
|
||||||
|
for (let i = 0; i < 5; i++) tick();
|
||||||
|
expect(app.drawn).toBe(0);
|
||||||
|
|
||||||
|
const waited = afterFrames(1);
|
||||||
|
tick();
|
||||||
|
await waited;
|
||||||
|
expect(app.drawn).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('afterFrames gives up rather than hanging when no frame ever comes', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
const { app } = fakeApp();
|
||||||
|
initRenderLoop(app as unknown as Application);
|
||||||
|
const waited = afterFrames(1, 50);
|
||||||
|
vi.advanceTimersByTime(60);
|
||||||
|
await waited;
|
||||||
|
expect(app.drawn).toBe(0);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+27
-2
@@ -11,7 +11,7 @@
|
|||||||
* rely on what earlier ones leave behind, a subset that skips a prerequisite fails on purpose.
|
* rely on what earlier ones leave behind, a subset that skips a prerequisite fails on purpose.
|
||||||
*/
|
*/
|
||||||
import { STEPS } from './e2e/index.mjs';
|
import { STEPS } from './e2e/index.mjs';
|
||||||
import { runChecks } from './lib/harness.mjs';
|
import { runChecks, waited } from './lib/harness.mjs';
|
||||||
import { PROJECTS, cliArgs } from './lib/projects.mjs';
|
import { PROJECTS, cliArgs } from './lib/projects.mjs';
|
||||||
|
|
||||||
const { projectId, baseUrl } = cliArgs('e2e.mjs', PROJECTS.fixture);
|
const { projectId, baseUrl } = cliArgs('e2e.mjs', PROJECTS.fixture);
|
||||||
@@ -35,8 +35,33 @@ if (steps.length !== STEPS.length) {
|
|||||||
console.log(`running ${steps.length}/${STEPS.length} steps: ${steps.map((s) => s.name).join(', ')}`);
|
console.log(`running ${steps.length}/${STEPS.length} steps: ${steps.map((s) => s.name).join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One line per step, printed as it finishes: how long it took and how much of that was the
|
||||||
|
* harness sitting out a fixed wait. It also puts the run's position into the log, so a run that
|
||||||
|
* dies half-way says where it stood instead of leaving the last check as the only clue.
|
||||||
|
*/
|
||||||
|
const timings = [];
|
||||||
|
async function timed(step, t) {
|
||||||
|
const at = Date.now();
|
||||||
|
const waitedAt = waited.ms;
|
||||||
|
await step(t);
|
||||||
|
const ms = Date.now() - at;
|
||||||
|
const slept = waited.ms - waitedAt;
|
||||||
|
timings.push({ name: step.name, ms, slept });
|
||||||
|
const no = String(STEPS.indexOf(step) + 1).padStart(2, '0');
|
||||||
|
console.log(`STEP ${no} ${step.name} – ${(ms / 1000).toFixed(1)}s (waited ${(slept / 1000).toFixed(1)}s)`);
|
||||||
|
}
|
||||||
|
|
||||||
await runChecks({ projectId, baseUrl, port: 9333, shotPrefix: 'gm-e2e' }, async (t) => {
|
await runChecks({ projectId, baseUrl, port: 9333, shotPrefix: 'gm-e2e' }, async (t) => {
|
||||||
await t.navigate();
|
await t.navigate();
|
||||||
await t.shot('01-loaded');
|
await t.shot('01-loaded');
|
||||||
for (const step of steps) await step(t);
|
for (const step of steps) await timed(step, t);
|
||||||
|
const total = timings.reduce((sum, s) => sum + s.ms, 0);
|
||||||
|
const slept = timings.reduce((sum, s) => sum + s.slept, 0);
|
||||||
|
const worst = [...timings].sort((a, b) => b.ms - a.ms).slice(0, 8);
|
||||||
|
console.log(
|
||||||
|
`\nTIMING ${(total / 1000).toFixed(0)}s in steps, ${(slept / 1000).toFixed(0)}s of it waiting ` +
|
||||||
|
`(${Math.round((slept / total) * 100)}%). Slowest: ` +
|
||||||
|
worst.map((s) => `${s.name} ${(s.ms / 1000).toFixed(1)}s`).join(', '),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
@@ -6,7 +6,7 @@ import { same } from '../lib/harness.mjs';
|
|||||||
* without touching the selection, and selecting a nested object unfolds the way down to it.
|
* without touching the selection, and selecting a nested object unfolds the way down to it.
|
||||||
*/
|
*/
|
||||||
export async function hierarchy(t) {
|
export async function hierarchy(t) {
|
||||||
const { check, sleep, evaluate, json, click, doubleClick, select, state, shot } = t;
|
const { check, evaluate, json, click, doubleClick, select, state, shot } = t;
|
||||||
const rowIds = () => json('__gm.hierarchyOrder()');
|
const rowIds = () => json('__gm.hierarchyOrder()');
|
||||||
const rows = async () => JSON.stringify(await rowIds());
|
const rows = async () => JSON.stringify(await rowIds());
|
||||||
const selectionBefore = (await state()).selection;
|
const selectionBefore = (await state()).selection;
|
||||||
@@ -48,13 +48,11 @@ export async function hierarchy(t) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'oc1' });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'oc1' });
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'selecting a nested object unfolds its ancestors',
|
'selecting a nested object unfolds its ancestors',
|
||||||
same(await rowIds(), ['o1', 'o4', 'oc1', 'o2', 'o3', 'o5']),
|
same(await rowIds(), ['o1', 'o4', 'oc1', 'o2', 'o3', 'o5']),
|
||||||
await rows(),
|
await rows(),
|
||||||
);
|
);
|
||||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||||
await sleep(150);
|
|
||||||
check('Collapse all folds everything back', same(await rowIds(), ['o1', 'o2', 'o3', 'o5']), await rows());
|
check('Collapse all folds everything back', same(await rowIds(), ['o1', 'o2', 'o3', 'o5']), await rows());
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,6 @@ export async function inspector(t) {
|
|||||||
await typeText('Renamed crate');
|
await typeText('Renamed crate');
|
||||||
await shot('03-typing');
|
await shot('03-typing');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'typing + Enter renamed the object',
|
'typing + Enter renamed the object',
|
||||||
(await object('o2')).name === 'Renamed crate',
|
(await object('o2')).name === 'Renamed crate',
|
||||||
@@ -77,7 +76,6 @@ export async function inspector(t) {
|
|||||||
await doubleClick(nameLabel.x + 160, nameLabel.y);
|
await doubleClick(nameLabel.x + 160, nameLabel.y);
|
||||||
await typeText('Twice');
|
await typeText('Twice');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'double-clicking a text field selects all – typing replaces the name',
|
'double-clicking a text field selects all – typing replaces the name',
|
||||||
(await object('o2')).name === 'Twice',
|
(await object('o2')).name === 'Twice',
|
||||||
@@ -86,7 +84,6 @@ export async function inspector(t) {
|
|||||||
// Undone rather than typed back: check 2h further down counts its undos, so this must not
|
// Undone rather than typed back: check 2h further down counts its undos, so this must not
|
||||||
// leave a step on the stack.
|
// leave a step on the stack.
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'undo takes that rename back',
|
'undo takes that rename back',
|
||||||
(await object('o2')).name === 'Renamed crate',
|
(await object('o2')).name === 'Renamed crate',
|
||||||
@@ -172,7 +169,6 @@ export async function inspector(t) {
|
|||||||
JSON.stringify(stillTyping),
|
JSON.stringify(stillTyping),
|
||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(200);
|
|
||||||
const after = await json('__gm.inspectorInfo()');
|
const after = await json('__gm.inspectorInfo()');
|
||||||
check(
|
check(
|
||||||
'leaving the field runs the postponed rebuild',
|
'leaving the field runs the postponed rebuild',
|
||||||
|
|||||||
+4
-12
@@ -9,7 +9,6 @@ const FIXTURE_IDS = ['o1', 'o2', 'o3', 'o4', 'o5', 'oc1', 'oc2'];
|
|||||||
export async function play(t) {
|
export async function play(t) {
|
||||||
const {
|
const {
|
||||||
check,
|
check,
|
||||||
sleep,
|
|
||||||
evaluate,
|
evaluate,
|
||||||
json,
|
json,
|
||||||
shot,
|
shot,
|
||||||
@@ -27,6 +26,7 @@ export async function play(t) {
|
|||||||
select,
|
select,
|
||||||
mutate,
|
mutate,
|
||||||
stop,
|
stop,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
const gameInfo = () => json('__gm.gameInfo()');
|
const gameInfo = () => json('__gm.gameInfo()');
|
||||||
const play = t.play;
|
const play = t.play;
|
||||||
@@ -92,7 +92,6 @@ export async function play(t) {
|
|||||||
);
|
);
|
||||||
await shot('08b-editing-while-playing');
|
await shot('08b-editing-while-playing');
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(150);
|
|
||||||
const liveIds = (await world()).entities.map((e) => e.objectId);
|
const liveIds = (await world()).entities.map((e) => e.objectId);
|
||||||
check(
|
check(
|
||||||
'Delete while playing removes the entity and its collider child from the world',
|
'Delete while playing removes the entity and its collider child from the world',
|
||||||
@@ -112,7 +111,6 @@ export async function play(t) {
|
|||||||
// Stop discards it with the rest of the runtime copy.
|
// Stop discards it with the rest of the runtime copy.
|
||||||
const visibleBeforeSpawn = (await gameInfo()).visibleItems;
|
const visibleBeforeSpawn = (await gameInfo()).visibleItems;
|
||||||
await press('Space');
|
await press('Space');
|
||||||
await sleep(200);
|
|
||||||
const spawnedObjects = await objects();
|
const spawnedObjects = await objects();
|
||||||
const spawned = spawnedObjects.find((o) => !FIXTURE_IDS.includes(o.id));
|
const spawned = spawnedObjects.find((o) => !FIXTURE_IDS.includes(o.id));
|
||||||
check(
|
check(
|
||||||
@@ -162,7 +160,6 @@ export async function play(t) {
|
|||||||
// 3d. Without a camera nothing is rendered: delete the camera object, play, then undo.
|
// 3d. Without a camera nothing is rendered: delete the camera object, play, then undo.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o3' });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'o3' });
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(200);
|
|
||||||
check('camera object deleted', (await objects()).length === 6);
|
check('camera object deleted', (await objects()).length === 6);
|
||||||
await play();
|
await play();
|
||||||
info = await gameInfo();
|
info = await gameInfo();
|
||||||
@@ -185,7 +182,6 @@ export async function play(t) {
|
|||||||
// Found through its row, not an offset from the label: the label column's width is layout.
|
// Found through its row, not an offset from the label: the label column's width is layout.
|
||||||
const enabledRow = await json("__gm.inspectorField('Enabled')");
|
const enabledRow = await json("__gm.inspectorField('Enabled')");
|
||||||
await click(enabledRow.x, enabledRow.y);
|
await click(enabledRow.x, enabledRow.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'the camera has the enabled switch like any other object',
|
'the camera has the enabled switch like any other object',
|
||||||
(await object('o3')).overrides.enabled === false,
|
(await object('o3')).overrides.enabled === false,
|
||||||
@@ -222,7 +218,6 @@ export async function play(t) {
|
|||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o3' });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'o3' });
|
||||||
const switchBackOn = await json("__gm.inspectorField('Enabled')");
|
const switchBackOn = await json("__gm.inspectorField('Enabled')");
|
||||||
await click(switchBackOn.x, switchBackOn.y);
|
await click(switchBackOn.x, switchBackOn.y);
|
||||||
await sleep(150);
|
|
||||||
await play();
|
await play();
|
||||||
info = await gameInfo();
|
info = await gameInfo();
|
||||||
check(
|
check(
|
||||||
@@ -249,8 +244,7 @@ export async function play(t) {
|
|||||||
const p = await json(`__gm.gameScreen('o1')`);
|
const p = await json(`__gm.gameScreen('o1')`);
|
||||||
return json(`__gm.pixelAt(${Math.round(p.x)}, ${Math.round(p.y)})`);
|
return json(`__gm.pixelAt(${Math.round(p.x)}, ${Math.round(p.y)})`);
|
||||||
};
|
};
|
||||||
await evaluate(`__gm.showTab('game')`);
|
await showTab('game');
|
||||||
await sleep(150);
|
|
||||||
const preview = await gameInfo();
|
const preview = await gameInfo();
|
||||||
const previewPixel = await crateSpot();
|
const previewPixel = await crateSpot();
|
||||||
check(
|
check(
|
||||||
@@ -276,11 +270,10 @@ export async function play(t) {
|
|||||||
await stop();
|
await stop();
|
||||||
|
|
||||||
// Switching the only camera off leaves nothing to preview – and says so, live on the tab.
|
// Switching the only camera off leaves nothing to preview – and says so, live on the tab.
|
||||||
await evaluate(`__gm.showTab('game')`);
|
await showTab('game');
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o3' });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'o3' });
|
||||||
const previewSwitch = await json("__gm.inspectorField('Enabled')");
|
const previewSwitch = await json("__gm.inspectorField('Enabled')");
|
||||||
await click(previewSwitch.x, previewSwitch.y);
|
await click(previewSwitch.x, previewSwitch.y);
|
||||||
await sleep(200);
|
|
||||||
info = await gameInfo();
|
info = await gameInfo();
|
||||||
check(
|
check(
|
||||||
'switching the camera off empties the preview and names the reason',
|
'switching the camera off empties the preview and names the reason',
|
||||||
@@ -289,14 +282,13 @@ export async function play(t) {
|
|||||||
);
|
);
|
||||||
await shot('10d-no-active-camera');
|
await shot('10d-no-active-camera');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
info = await gameInfo();
|
info = await gameInfo();
|
||||||
check(
|
check(
|
||||||
'and undo brings the preview back without a reload',
|
'and undo brings the preview back without a reload',
|
||||||
info.rendering && info.camera === 'o3' && (await json(`__gm.locate('No active camera')`)) === null,
|
info.rendering && info.camera === 'o3' && (await json(`__gm.locate('No active camera')`)) === null,
|
||||||
JSON.stringify(info),
|
JSON.stringify(info),
|
||||||
);
|
);
|
||||||
await evaluate(`__gm.showTab('scene')`);
|
await showTab('scene');
|
||||||
check(
|
check(
|
||||||
'the fixture is left as it was found',
|
'the fixture is left as it was found',
|
||||||
(await objects()).length === 7 && Object.keys((await object('o3')).overrides).length === 0,
|
(await objects()).length === 7 && Object.keys((await object('o3')).overrides).length === 0,
|
||||||
|
|||||||
+5
-19
@@ -17,6 +17,7 @@ export async function assets(t) {
|
|||||||
select,
|
select,
|
||||||
objects,
|
objects,
|
||||||
asset,
|
asset,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
|
|
||||||
// 4a. Context menu on the Crate asset. Placing left the menu when it became a drag.
|
// 4a. Context menu on the Crate asset. Placing left the menu when it became a drag.
|
||||||
@@ -25,7 +26,6 @@ export async function assets(t) {
|
|||||||
check('placing is no longer a menu item', !(await evaluate(`__gm.locate('Place in scene')`)));
|
check('placing is no longer a menu item', !(await evaluate(`__gm.locate('Place in scene')`)));
|
||||||
await shot('11-context-menu');
|
await shot('11-context-menu');
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
// 4b. Drag the Crate asset from the panel into the scene view. Driven step by step rather than
|
// 4b. Drag the Crate asset from the panel into the scene view. Driven step by step rather than
|
||||||
// with `drag()`, because the preview only exists while the button is still down.
|
// with `drag()`, because the preview only exists while the button is still down.
|
||||||
@@ -70,14 +70,12 @@ export async function assets(t) {
|
|||||||
const afterUndo = (await objects()).length;
|
const afterUndo = (await objects()).length;
|
||||||
check('one undo takes the dropped object back out', afterUndo === 7, `objects=${afterUndo}`);
|
check('one undo takes the dropped object back out', afterUndo === 7, `objects=${afterUndo}`);
|
||||||
await redo();
|
await redo();
|
||||||
await sleep(150);
|
|
||||||
check('redo puts it back', (await objects()).length === 8);
|
check('redo puts it back', (await objects()).length === 8);
|
||||||
|
|
||||||
// 4c. Create a new asset through the + menu. Since #43 the menu is two alphabetical groups –
|
// 4c. Create a new asset through the + menu. Since #43 the menu is two alphabetical groups –
|
||||||
// what can be placed in a scene, a line, then what only lives in the library – and it is
|
// what can be placed in a scene, a line, then what only lives in the library – and it is
|
||||||
// tall enough to show all fifteen types without scrolling.
|
// tall enough to show all fifteen types without scrolling.
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await sleep(150);
|
|
||||||
const rows = await json('__gm.menuRows()');
|
const rows = await json('__gm.menuRows()');
|
||||||
const cut = rows.indexOf(null);
|
const cut = rows.indexOf(null);
|
||||||
const placeable = rows.slice(0, cut);
|
const placeable = rows.slice(0, cut);
|
||||||
@@ -129,7 +127,6 @@ export async function assets(t) {
|
|||||||
JSON.stringify({ above, below, margin, screenH }),
|
JSON.stringify({ above, below, margin, screenH }),
|
||||||
);
|
);
|
||||||
await clickText('Sprite');
|
await clickText('Sprite');
|
||||||
await sleep(100);
|
|
||||||
const names = await json('__gm.state.project.assets.map(a => a.name)');
|
const names = await json('__gm.state.project.assets.map(a => a.name)');
|
||||||
check('+ menu creates a sprite asset', names.includes('Sprite'), JSON.stringify(names));
|
check('+ menu creates a sprite asset', names.includes('Sprite'), JSON.stringify(names));
|
||||||
await shot('12-new-asset');
|
await shot('12-new-asset');
|
||||||
@@ -141,8 +138,7 @@ export async function assets(t) {
|
|||||||
`__gm.state.project.assets.push({ id: 'scene2', type: 'scene', name: 'Level 2', props: { backgroundColor: 0x22252c }, objects: [] })`,
|
`__gm.state.project.assets.push({ id: 'scene2', type: 'scene', name: 'Level 2', props: { backgroundColor: 0x22252c }, objects: [] })`,
|
||||||
);
|
);
|
||||||
const secondScene = await json("__gm.assetRow('scene2')");
|
const secondScene = await json("__gm.assetRow('scene2')");
|
||||||
await evaluate("__gm.showTab('graph')");
|
await showTab('graph');
|
||||||
await sleep(150);
|
|
||||||
await doubleClick(secondScene.x, secondScene.y);
|
await doubleClick(secondScene.x, secondScene.y);
|
||||||
check(
|
check(
|
||||||
'double-clicking a scene asset makes it the active scene',
|
'double-clicking a scene asset makes it the active scene',
|
||||||
@@ -177,8 +173,7 @@ export async function assets(t) {
|
|||||||
['box-open', 'container', '__gm.container.current()'],
|
['box-open', 'container', '__gm.container.current()'],
|
||||||
];
|
];
|
||||||
for (const [id, tab, shown] of openable) {
|
for (const [id, tab, shown] of openable) {
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(150);
|
|
||||||
const row = await json(`__gm.assetRow(${JSON.stringify(id)})`);
|
const row = await json(`__gm.assetRow(${JSON.stringify(id)})`);
|
||||||
await click(row.x, row.y);
|
await click(row.x, row.y);
|
||||||
await sleep(500); // past the double-click window, so the next press starts a fresh pair
|
await sleep(500); // past the double-click window, so the next press starts a fresh pair
|
||||||
@@ -192,7 +187,6 @@ export async function assets(t) {
|
|||||||
JSON.stringify(clicked),
|
JSON.stringify(clicked),
|
||||||
);
|
);
|
||||||
await doubleClick(row.x, row.y);
|
await doubleClick(row.x, row.y);
|
||||||
await sleep(300);
|
|
||||||
const opened = { tab: await json('__gm.tab()'), shown: await json(shown) };
|
const opened = { tab: await json('__gm.tab()'), shown: await json(shown) };
|
||||||
check(
|
check(
|
||||||
`a double-click opens the ${tab} tab on it`,
|
`a double-click opens the ${tab} tab on it`,
|
||||||
@@ -202,13 +196,10 @@ export async function assets(t) {
|
|||||||
}
|
}
|
||||||
// Enter goes the same way (`onActivate`), but only once the list holds keyboard focus, which a
|
// Enter goes the same way (`onActivate`), but only once the list holds keyboard focus, which a
|
||||||
// click does not give it – so it is not driven here.
|
// click does not give it – so it is not driven here.
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(150);
|
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await sleep(200);
|
|
||||||
const entry = await evaluate(`__gm.locate('Behavior', true)`);
|
const entry = await evaluate(`__gm.locate('Behavior', true)`);
|
||||||
await click(entry.x, entry.y);
|
await click(entry.x, entry.y);
|
||||||
await sleep(400);
|
|
||||||
const created = await json(
|
const created = await json(
|
||||||
"(__gm.state.project.assets.find((a) => a.type === 'behavior' && !['mover', 'spawner'].includes(a.id)) ?? {}).id ?? ''",
|
"(__gm.state.project.assets.find((a) => a.type === 'behavior' && !['mover', 'spawner'].includes(a.id)) ?? {}).id ?? ''",
|
||||||
);
|
);
|
||||||
@@ -222,14 +213,12 @@ export async function assets(t) {
|
|||||||
await mutate(
|
await mutate(
|
||||||
`__gm.state.project.assets = __gm.state.project.assets.filter((a) => !['sm-open', 'tex-open', 'anim-open', 'box-open'].includes(a.id))`,
|
`__gm.state.project.assets = __gm.state.project.assets.filter((a) => !['sm-open', 'tex-open', 'anim-open', 'box-open'].includes(a.id))`,
|
||||||
);
|
);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
// 4e. Asset defaults: the Crate deviates in height (60 against the schema's 100) and colour,
|
// 4e. 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
|
// 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.
|
// reset is the first on screen and colour's the last.
|
||||||
await select({ kind: 'asset', assetId: 'crate' });
|
await select({ kind: 'asset', assetId: 'crate' });
|
||||||
await sleep(200);
|
|
||||||
const heightReset = await evaluate(`__gm.locate('↺')`);
|
const heightReset = await evaluate(`__gm.locate('↺')`);
|
||||||
const colorReset = await evaluate(`__gm.locate('↺', true)`);
|
const colorReset = await evaluate(`__gm.locate('↺', true)`);
|
||||||
check('asset inspector shows a reset per deviating property', Boolean(heightReset));
|
check('asset inspector shows a reset per deviating property', Boolean(heightReset));
|
||||||
@@ -239,7 +228,6 @@ export async function assets(t) {
|
|||||||
JSON.stringify({ heightReset, colorReset }),
|
JSON.stringify({ heightReset, colorReset }),
|
||||||
);
|
);
|
||||||
await click(heightReset.x, heightReset.y);
|
await click(heightReset.x, heightReset.y);
|
||||||
await sleep(100);
|
|
||||||
check(
|
check(
|
||||||
'reset puts the asset property back to its default',
|
'reset puts the asset property back to its default',
|
||||||
(await asset('crate')).props.height === 100,
|
(await asset('crate')).props.height === 100,
|
||||||
@@ -281,7 +269,6 @@ export async function assets(t) {
|
|||||||
await assetColor('mover'),
|
await assetColor('mover'),
|
||||||
);
|
);
|
||||||
await click(heightReset.x, heightReset.y);
|
await click(heightReset.x, heightReset.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'the mark stays while the second deviation is left',
|
'the mark stays while the second deviation is left',
|
||||||
(await assetColor('crate')) === GOLD,
|
(await assetColor('crate')) === GOLD,
|
||||||
@@ -289,7 +276,6 @@ export async function assets(t) {
|
|||||||
);
|
);
|
||||||
const colorOnly = await evaluate(`__gm.locate('↺')`);
|
const colorOnly = await evaluate(`__gm.locate('↺')`);
|
||||||
await click(colorOnly.x, colorOnly.y);
|
await click(colorOnly.x, colorOnly.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'resetting the last deviation clears the mark, without a reload',
|
'resetting the last deviation clears the mark, without a reload',
|
||||||
(await assetColor('crate')) === PLAIN,
|
(await assetColor('crate')) === PLAIN,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** Draw order: symbols always above sprites, `z` above hierarchy order. */
|
/** Draw order: symbols always above sprites, `z` above hierarchy order. */
|
||||||
export async function drawing(t) {
|
export async function drawing(t) {
|
||||||
const { check, sleep, json, shot, click, locate, typeText, press, undo, mutate, select, object } = t;
|
const { check, json, shot, click, locate, typeText, press, undo, mutate, select, object } = t;
|
||||||
const orderOfCrates = () => json(`__gm.drawOrder().filter(id => id === 'o1' || id === 'o2')`);
|
const orderOfCrates = () => json(`__gm.drawOrder().filter(id => id === 'o1' || id === 'o2')`);
|
||||||
|
|
||||||
// 5a. Symbols stay on top: put the camera first in the hierarchy (drawn first) and a crate
|
// 5a. Symbols stay on top: put the camera first in the hierarchy (drawn first) and a crate
|
||||||
@@ -31,12 +31,10 @@ export async function drawing(t) {
|
|||||||
JSON.stringify(await orderOfCrates()),
|
JSON.stringify(await orderOfCrates()),
|
||||||
);
|
);
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1' });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1' });
|
||||||
await sleep(200);
|
|
||||||
const zLabel = await locate('Z');
|
const zLabel = await locate('Z');
|
||||||
await click(zLabel.x + 160, zLabel.y); // plain click → text editing with everything selected
|
await click(zLabel.x + 160, zLabel.y); // plain click → text editing with everything selected
|
||||||
await typeText('5');
|
await typeText('5');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'Z field sets position.z',
|
'Z field sets position.z',
|
||||||
(await object('o1')).position.z === 5,
|
(await object('o1')).position.z === 5,
|
||||||
|
|||||||
+9
-73
@@ -6,6 +6,7 @@ export async function graph(t) {
|
|||||||
const {
|
const {
|
||||||
check,
|
check,
|
||||||
sleep,
|
sleep,
|
||||||
|
saved,
|
||||||
evaluate,
|
evaluate,
|
||||||
json,
|
json,
|
||||||
find,
|
find,
|
||||||
@@ -26,6 +27,7 @@ export async function graph(t) {
|
|||||||
api,
|
api,
|
||||||
navigate,
|
navigate,
|
||||||
projectId,
|
projectId,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
const nodeOf = async (id) => (await asset('mover')).graph.nodes.find((n) => n.id === id);
|
const nodeOf = async (id) => (await asset('mover')).graph.nodes.find((n) => n.id === id);
|
||||||
const graphCounts = async () => {
|
const graphCounts = async () => {
|
||||||
@@ -36,8 +38,7 @@ export async function graph(t) {
|
|||||||
|
|
||||||
// 6a. Selecting the Mover asset binds the graph; the Behavior tab shows it.
|
// 6a. Selecting the Mover asset binds the graph; the Behavior tab shows it.
|
||||||
await clickText('Mover');
|
await clickText('Mover');
|
||||||
await evaluate(`__gm.showTab('graph')`);
|
await showTab('graph');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'graph editor shows the selected behavior',
|
'graph editor shows the selected behavior',
|
||||||
(await evaluate(`__gm.graph.current()`)) === 'mover' && (await evaluate('__gm.tab()')) === 'graph',
|
(await evaluate(`__gm.graph.current()`)) === 'mover' && (await evaluate('__gm.tab()')) === 'graph',
|
||||||
@@ -79,13 +80,10 @@ export async function graph(t) {
|
|||||||
const bare = await json('__gm.graph.emptySpot()');
|
const bare = await json('__gm.graph.emptySpot()');
|
||||||
const beforeClickPick = await graphCounts();
|
const beforeClickPick = await graphCounts();
|
||||||
await click(bare.x, bare.y, 'right');
|
await click(bare.x, bare.y, 'right');
|
||||||
await sleep(100);
|
|
||||||
check('node palette opens (row-click case)', Boolean(await evaluate(`__gm.locate('Search nodes…')`)));
|
check('node palette opens (row-click case)', Boolean(await evaluate(`__gm.locate('Search nodes…')`)));
|
||||||
await typeText('stop');
|
await typeText('stop');
|
||||||
await sleep(150);
|
|
||||||
const stopRow = await locate('Stop game'); // nothing else on screen shows this label
|
const stopRow = await locate('Stop game'); // nothing else on screen shows this label
|
||||||
await click(stopRow.x, stopRow.y);
|
await click(stopRow.x, stopRow.y);
|
||||||
await sleep(100);
|
|
||||||
const afterClickPick = await graphCounts();
|
const afterClickPick = await graphCounts();
|
||||||
check(
|
check(
|
||||||
'clicking a palette row adds exactly one node',
|
'clicking a palette row adds exactly one node',
|
||||||
@@ -99,7 +97,6 @@ export async function graph(t) {
|
|||||||
// 6d. Right-click the empty canvas → palette → search "log" → Enter adds a Log node.
|
// 6d. Right-click the empty canvas → palette → search "log" → Enter adds a Log node.
|
||||||
const before13 = await graphCounts();
|
const before13 = await graphCounts();
|
||||||
await click(bare.x, bare.y, 'right');
|
await click(bare.x, bare.y, 'right');
|
||||||
await sleep(100);
|
|
||||||
check('node palette opens', Boolean(await evaluate(`__gm.locate('Search nodes…')`)));
|
check('node palette opens', Boolean(await evaluate(`__gm.locate('Search nodes…')`)));
|
||||||
// Unfiltered the palette holds far more nodes than fit, so it grows to the full height it may
|
// Unfiltered the palette holds far more nodes than fit, so it grows to the full height it may
|
||||||
// have – and that height is the canvas minus a tenth at each end. Shifting it up to fit used to
|
// have – and that height is the canvas minus a tenth at each end. Shifting it up to fit used to
|
||||||
@@ -116,9 +113,7 @@ export async function graph(t) {
|
|||||||
JSON.stringify({ gapAbove, gapBelow, paletteMargin, paletteScreenH }),
|
JSON.stringify({ gapAbove, gapBelow, paletteMargin, paletteScreenH }),
|
||||||
);
|
);
|
||||||
await typeText('log');
|
await typeText('log');
|
||||||
await sleep(100);
|
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(250);
|
|
||||||
const after13 = await graphCounts();
|
const after13 = await graphCounts();
|
||||||
s = await state();
|
s = await state();
|
||||||
const newNode = s.selection?.kind === 'node' ? await nodeOf(s.selection.nodeId) : null;
|
const newNode = s.selection?.kind === 'node' ? await nodeOf(s.selection.nodeId) : null;
|
||||||
@@ -134,9 +129,7 @@ export async function graph(t) {
|
|||||||
// Enter the first row alphabetically, which was `Run behavior`.
|
// Enter the first row alphabetically, which was `Run behavior`.
|
||||||
const beforeRank = await graphCounts();
|
const beforeRank = await graphCounts();
|
||||||
await click(bare.x, bare.y, 'right');
|
await click(bare.x, bare.y, 'right');
|
||||||
await sleep(100);
|
|
||||||
await typeText('variable');
|
await typeText('variable');
|
||||||
await sleep(150);
|
|
||||||
const ranked = await json('__gm.graph.paletteRows()');
|
const ranked = await json('__gm.graph.paletteRows()');
|
||||||
const hits = (row) => `${row.label} ${row.type}`.toLowerCase().includes('variable');
|
const hits = (row) => `${row.label} ${row.type}`.toLowerCase().includes('variable');
|
||||||
const lastNamed = ranked.map(hits).lastIndexOf(true);
|
const lastNamed = ranked.map(hits).lastIndexOf(true);
|
||||||
@@ -147,7 +140,6 @@ export async function graph(t) {
|
|||||||
JSON.stringify(ranked),
|
JSON.stringify(ranked),
|
||||||
);
|
);
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(250);
|
|
||||||
s = await state();
|
s = await state();
|
||||||
const rankPick = s.selection?.kind === 'node' ? await nodeOf(s.selection.nodeId) : null;
|
const rankPick = s.selection?.kind === 'node' ? await nodeOf(s.selection.nodeId) : null;
|
||||||
check(
|
check(
|
||||||
@@ -156,7 +148,6 @@ export async function graph(t) {
|
|||||||
JSON.stringify(rankPick),
|
JSON.stringify(rankPick),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(150);
|
|
||||||
check('undo removes it again', (await graphCounts()).nodes === beforeRank.nodes);
|
check('undo removes it again', (await graphCounts()).nodes === beforeRank.nodes);
|
||||||
|
|
||||||
// 6e. Connect: drag from the Move node's exec output onto the Log node's exec input. An
|
// 6e. Connect: drag from the Move node's exec output onto the Log node's exec input. An
|
||||||
@@ -240,7 +231,6 @@ export async function graph(t) {
|
|||||||
await shot('18c-drop-into-empty');
|
await shot('18c-drop-into-empty');
|
||||||
const clampRow = await locate('Clamp');
|
const clampRow = await locate('Clamp');
|
||||||
await click(clampRow.x, clampRow.y);
|
await click(clampRow.x, clampRow.y);
|
||||||
await sleep(200);
|
|
||||||
const afterDrop = await graphCounts();
|
const afterDrop = await graphCounts();
|
||||||
const wired = await state();
|
const wired = await state();
|
||||||
const clampEdge = (await asset('mover')).graph.edges.find(
|
const clampEdge = (await asset('mover')).graph.edges.find(
|
||||||
@@ -272,7 +262,6 @@ export async function graph(t) {
|
|||||||
await mouse('mouseReleased', empty.x, empty.y);
|
await mouse('mouseReleased', empty.x, empty.y);
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(150);
|
|
||||||
const cancelled = await graphCounts();
|
const cancelled = await graphCounts();
|
||||||
check(
|
check(
|
||||||
'escaping the palette clears the edge and adds nothing',
|
'escaping the palette clears the edge and adds nothing',
|
||||||
@@ -284,10 +273,8 @@ export async function graph(t) {
|
|||||||
|
|
||||||
// 6f. Delete removes the selected node together with its edge; undo brings both back.
|
// 6f. Delete removes the selected node together with its edge; undo brings both back.
|
||||||
await select({ kind: 'node', assetId: 'mover', nodeId: newNode.id });
|
await select({ kind: 'node', assetId: 'mover', nodeId: newNode.id });
|
||||||
await sleep(200);
|
|
||||||
check('the node inspector has no delete button of its own', !(await t.has('Delete node')));
|
check('the node inspector has no delete button of its own', !(await t.has('Delete node')));
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(150);
|
|
||||||
const deleted = await graphCounts();
|
const deleted = await graphCounts();
|
||||||
check(
|
check(
|
||||||
'Delete removes the node and its edge',
|
'Delete removes the node and its edge',
|
||||||
@@ -324,8 +311,7 @@ export async function graph(t) {
|
|||||||
typeof fpsPlay === 'number' && fpsPlay > 0,
|
typeof fpsPlay === 'number' && fpsPlay > 0,
|
||||||
`fps=${fpsPlay}`,
|
`fps=${fpsPlay}`,
|
||||||
);
|
);
|
||||||
await evaluate(`__gm.showTab('graph')`);
|
await showTab('graph');
|
||||||
await sleep(100);
|
|
||||||
const holding = holdKey('ArrowRight', 300);
|
const holding = holdKey('ArrowRight', 300);
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
await shot('19-graph-live');
|
await shot('19-graph-live');
|
||||||
@@ -334,10 +320,8 @@ export async function graph(t) {
|
|||||||
|
|
||||||
// 6h. Variables: the "+ Variable" dropdown → Number adds "var1" with default 0; undo removes it.
|
// 6h. Variables: the "+ Variable" dropdown → Number adds "var1" with default 0; undo removes it.
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(150);
|
|
||||||
await clickText('+ Variable');
|
await clickText('+ Variable');
|
||||||
await clickText('Number');
|
await clickText('Number');
|
||||||
await sleep(100);
|
|
||||||
check(
|
check(
|
||||||
'adding a variable declares it on the behavior',
|
'adding a variable declares it on the behavior',
|
||||||
(await variableKeys()) === 'speed,var1',
|
(await variableKeys()) === 'speed,var1',
|
||||||
@@ -358,13 +342,11 @@ export async function graph(t) {
|
|||||||
);
|
);
|
||||||
check('the header has no Add node button – the right-click palette is the way', !(await find('Add node…')));
|
check('the header has no Add node button – the right-click palette is the way', !(await find('Add node…')));
|
||||||
await select(null);
|
await select(null);
|
||||||
await sleep(100);
|
|
||||||
check(
|
check(
|
||||||
'graph editor keeps its behavior when nothing is selected',
|
'graph editor keeps its behavior when nothing is selected',
|
||||||
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
||||||
);
|
);
|
||||||
await select({ kind: 'asset', assetId: 'cam' });
|
await select({ kind: 'asset', assetId: 'cam' });
|
||||||
await sleep(100);
|
|
||||||
check(
|
check(
|
||||||
'graph editor keeps its behavior for a non-behavior asset',
|
'graph editor keeps its behavior for a non-behavior asset',
|
||||||
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
||||||
@@ -372,17 +354,14 @@ export async function graph(t) {
|
|||||||
// The picker is the *last* "Mover" on screen: the assets panel shows one too.
|
// The picker is the *last* "Mover" on screen: the assets panel shows one too.
|
||||||
const pickerAt = await locate('Mover', true);
|
const pickerAt = await locate('Mover', true);
|
||||||
await click(pickerAt.x, pickerAt.y);
|
await click(pickerAt.x, pickerAt.y);
|
||||||
await sleep(150);
|
|
||||||
const spawnerRow = await locate('Spawner', true);
|
const spawnerRow = await locate('Spawner', true);
|
||||||
await click(spawnerRow.x, spawnerRow.y);
|
await click(spawnerRow.x, spawnerRow.y);
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'the header picker switches the graph to another behavior',
|
'the header picker switches the graph to another behavior',
|
||||||
(await evaluate(`__gm.graph.current()`)) === 'spawner',
|
(await evaluate(`__gm.graph.current()`)) === 'spawner',
|
||||||
String(await evaluate(`__gm.graph.current()`)),
|
String(await evaluate(`__gm.graph.current()`)),
|
||||||
);
|
);
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(100);
|
|
||||||
check(
|
check(
|
||||||
'graph editor rebinds when a behavior is selected again',
|
'graph editor rebinds when a behavior is selected again',
|
||||||
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
||||||
@@ -391,7 +370,6 @@ export async function graph(t) {
|
|||||||
// 6j. Mouse behaviour, as in the scene view: the middle button pans from anywhere – a node
|
// 6j. Mouse behaviour, as in the scene view: the middle button pans from anywhere – a node
|
||||||
// under it included – and the left button no longer pans at all.
|
// under it included – and the left button no longer pans at all.
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(250);
|
|
||||||
const positions = async () =>
|
const positions = async () =>
|
||||||
(await asset('mover')).graph.nodes.map((n) => `${n.id}:${n.x},${n.y}`).join(' ');
|
(await asset('mover')).graph.nodes.map((n) => `${n.id}:${n.x},${n.y}`).join(' ');
|
||||||
const area = await json('__gm.graph.canvasRect()');
|
const area = await json('__gm.graph.canvasRect()');
|
||||||
@@ -415,7 +393,6 @@ export async function graph(t) {
|
|||||||
|
|
||||||
// Fit again: the pan just now pushed a node out of view, and a band takes only what it sees.
|
// Fit again: the pan just now pushed a node out of view, and a band takes only what it sees.
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(250);
|
|
||||||
// An empty spot: after a fit every node sits at least a margin away from the corner.
|
// An empty spot: after a fit every node sits at least a margin away from the corner.
|
||||||
const corner = { x: area.x + 6, y: area.y + 6 };
|
const corner = { x: area.x + 6, y: area.y + 6 };
|
||||||
const opposite = { x: area.x + area.w - 6, y: area.y + area.h - 6 };
|
const opposite = { x: area.x + area.w - 6, y: area.y + area.h - 6 };
|
||||||
@@ -464,7 +441,6 @@ export async function graph(t) {
|
|||||||
const snapLabel = await locate('Snap');
|
const snapLabel = await locate('Snap');
|
||||||
// The checkbox sits right of the "Snap" label; locate() gives the label's text centre.
|
// The checkbox sits right of the "Snap" label; locate() gives the label's text centre.
|
||||||
await click(snapLabel.x + 30, snapLabel.y);
|
await click(snapLabel.x + 30, snapLabel.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'the toggle switches it on without moving a single node',
|
'the toggle switches it on without moving a single node',
|
||||||
(await json('__gm.graph.snapping()')) === true && (await positions()) === before13i,
|
(await json('__gm.graph.snapping()')) === true && (await positions()) === before13i,
|
||||||
@@ -473,7 +449,6 @@ export async function graph(t) {
|
|||||||
|
|
||||||
// A single node: dropped between two grid lines, it lands on the nearer one.
|
// A single node: dropped between two grid lines, it lands on the nearer one.
|
||||||
await click(corner.x, corner.y);
|
await click(corner.x, corner.y);
|
||||||
await sleep(150);
|
|
||||||
const snapPre = await nodeOf('n-axis');
|
const snapPre = await nodeOf('n-axis');
|
||||||
const snapFrom = await json(`__gm.graph.nodeAt('n-axis')`);
|
const snapFrom = await json(`__gm.graph.nodeAt('n-axis')`);
|
||||||
const snapBy = await units(50);
|
const snapBy = await units(50);
|
||||||
@@ -521,9 +496,7 @@ export async function graph(t) {
|
|||||||
|
|
||||||
// Switching it off has to give the pointer its exact drop position back.
|
// Switching it off has to give the pointer its exact drop position back.
|
||||||
await click(snapLabel.x + 30, snapLabel.y);
|
await click(snapLabel.x + 30, snapLabel.y);
|
||||||
await sleep(150);
|
|
||||||
await click(corner.x, corner.y);
|
await click(corner.x, corner.y);
|
||||||
await sleep(150);
|
|
||||||
const offPre = await nodeOf('n-axis');
|
const offPre = await nodeOf('n-axis');
|
||||||
const offFrom = await json(`__gm.graph.nodeAt('n-axis')`);
|
const offFrom = await json(`__gm.graph.nodeAt('n-axis')`);
|
||||||
const offBy = await units(50);
|
const offBy = await units(50);
|
||||||
@@ -538,13 +511,11 @@ export async function graph(t) {
|
|||||||
`${offPre.x},${offPre.y} → ${offPost.x},${offPost.y}`,
|
`${offPre.x},${offPre.y} → ${offPost.x},${offPost.y}`,
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
// 6m. Delete acts on the selection, not just the primary node.
|
// 6m. Delete acts on the selection, not just the primary node.
|
||||||
await drag(corner.x, corner.y, opposite.x, opposite.y, { steps: 16 });
|
await drag(corner.x, corner.y, opposite.x, opposite.y, { steps: 16 });
|
||||||
const beforeDelete = await graphCounts();
|
const beforeDelete = await graphCounts();
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(200);
|
|
||||||
const afterDelete = await graphCounts();
|
const afterDelete = await graphCounts();
|
||||||
check(
|
check(
|
||||||
'Delete removes every selected node, edges with them',
|
'Delete removes every selected node, edges with them',
|
||||||
@@ -563,7 +534,6 @@ export async function graph(t) {
|
|||||||
// and a click cannot miss one that the earlier pan pushed over the edge.
|
// and a click cannot miss one that the earlier pan pushed over the edge.
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(250);
|
|
||||||
const axisRow = await json(`__gm.graph.nodeAt('n-axis')`);
|
const axisRow = await json(`__gm.graph.nodeAt('n-axis')`);
|
||||||
await click(axisRow.x, axisRow.y);
|
await click(axisRow.x, axisRow.y);
|
||||||
const second = (await asset('mover')).graph.nodes.find((n) => n.id !== 'n-axis');
|
const second = (await asset('mover')).graph.nodes.find((n) => n.id !== 'n-axis');
|
||||||
@@ -591,7 +561,6 @@ export async function graph(t) {
|
|||||||
);
|
);
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(300);
|
|
||||||
const rects = [];
|
const rects = [];
|
||||||
for (const node of (await asset('mover')).graph.nodes)
|
for (const node of (await asset('mover')).graph.nodes)
|
||||||
rects.push(await json(`__gm.graph.nodeRect(${JSON.stringify(node.id)})`));
|
rects.push(await json(`__gm.graph.nodeRect(${JSON.stringify(node.id)})`));
|
||||||
@@ -614,14 +583,12 @@ export async function graph(t) {
|
|||||||
`(() => { const n = __gm.state.project.assets.find(a => a.id === 'mover').graph.nodes.find(n => n.id === 'n-axis'); n.x -= ${park}; __gm.state.preview(); })()`,
|
`(() => { const n = __gm.state.project.assets.find(a => a.id === 'mover').graph.nodes.find(n => n.id === 'n-axis'); n.x -= ${park}; __gm.state.preview(); })()`,
|
||||||
);
|
);
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(250);
|
|
||||||
check('the parked node is back where it started', (await positions()) === before13i, await positions());
|
check('the parked node is back where it started', (await positions()) === before13i, await positions());
|
||||||
|
|
||||||
// 6p. Hiding a link: it stops being drawn across the graph but keeps carrying its value, and
|
// 6p. Hiding a link: it stops being drawn across the graph but keeps carrying its value, and
|
||||||
// comes back while either of its two nodes is selected.
|
// comes back while either of its two nodes is selected.
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(250);
|
|
||||||
const allEdges = (await asset('mover')).graph.edges;
|
const allEdges = (await asset('mover')).graph.edges;
|
||||||
const edge = allEdges.find((e) => e.from.port !== 'exec') ?? allEdges[0];
|
const edge = allEdges.find((e) => e.from.port !== 'exec') ?? allEdges[0];
|
||||||
const id = JSON.stringify(edge.id);
|
const id = JSON.stringify(edge.id);
|
||||||
@@ -633,12 +600,9 @@ export async function graph(t) {
|
|||||||
`__gm.graph.portAt(${JSON.stringify(edge.from.node)}, ${JSON.stringify(edge.from.port)}, true)`,
|
`__gm.graph.portAt(${JSON.stringify(edge.from.node)}, ${JSON.stringify(edge.from.port)}, true)`,
|
||||||
);
|
);
|
||||||
await click(outPort.x, outPort.y, 'right');
|
await click(outPort.x, outPort.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
check('right-clicking a port offers hiding its link', Boolean(await evaluate(`__gm.locate('Hide link')`)));
|
check('right-clicking a port offers hiding its link', Boolean(await evaluate(`__gm.locate('Hide link')`)));
|
||||||
await clickText('Hide link');
|
await clickText('Hide link');
|
||||||
await sleep(250);
|
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(150);
|
|
||||||
const hidden = await edgeNow();
|
const hidden = await edgeNow();
|
||||||
check(
|
check(
|
||||||
'hiding changes nothing but a flag – the connection is still there',
|
'hiding changes nothing but a flag – the connection is still there',
|
||||||
@@ -660,21 +624,17 @@ export async function graph(t) {
|
|||||||
// Selecting either end lends the curve back – the source first, then the target.
|
// Selecting either end lends the curve back – the source first, then the target.
|
||||||
const sourceAt = await json(`__gm.graph.nodeAt(${JSON.stringify(edge.from.node)})`);
|
const sourceAt = await json(`__gm.graph.nodeAt(${JSON.stringify(edge.from.node)})`);
|
||||||
await click(sourceAt.x, sourceAt.y);
|
await click(sourceAt.x, sourceAt.y);
|
||||||
await sleep(200);
|
|
||||||
check('selecting the node at one end brings the curve back', (await drawn()) === true);
|
check('selecting the node at one end brings the curve back', (await drawn()) === true);
|
||||||
const targetAt = await json(`__gm.graph.nodeAt(${JSON.stringify(edge.to.node)})`);
|
const targetAt = await json(`__gm.graph.nodeAt(${JSON.stringify(edge.to.node)})`);
|
||||||
await click(targetAt.x, targetAt.y);
|
await click(targetAt.x, targetAt.y);
|
||||||
await sleep(200);
|
|
||||||
check('selecting the node at the other end does too', (await drawn()) === true);
|
check('selecting the node at the other end does too', (await drawn()) === true);
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(200);
|
|
||||||
check('deselecting hides it again', (await drawn()) === false);
|
check('deselecting hides it again', (await drawn()) === false);
|
||||||
|
|
||||||
// The mark is the handle: clicking it selects the node at the far end, which is also what
|
// The mark is the handle: clicking it selects the node at the far end, which is also what
|
||||||
// puts the curve back – so it answers "where does this go?" in one click.
|
// puts the curve back – so it answers "where does this go?" in one click.
|
||||||
const mark2 = await json(`__gm.graph.foldMark(${id}, true)`);
|
const mark2 = await json(`__gm.graph.foldMark(${id}, true)`);
|
||||||
await click(mark2.x, mark2.y);
|
await click(mark2.x, mark2.y);
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'clicking the mark jumps to the node at the far end',
|
'clicking the mark jumps to the node at the far end',
|
||||||
JSON.stringify(await json('__gm.graph.selected()')) === JSON.stringify([edge.to.node]),
|
JSON.stringify(await json('__gm.graph.selected()')) === JSON.stringify([edge.to.node]),
|
||||||
@@ -684,7 +644,6 @@ export async function graph(t) {
|
|||||||
|
|
||||||
await undo();
|
await undo();
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(250);
|
|
||||||
const shown = await edgeNow();
|
const shown = await edgeNow();
|
||||||
check(
|
check(
|
||||||
'undo unhides the link, with nothing left behind',
|
'undo unhides the link, with nothing left behind',
|
||||||
@@ -737,12 +696,9 @@ export async function graph(t) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await click(forkPort.x, forkPort.y, 'right');
|
await click(forkPort.x, forkPort.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
check('the port menu counts what it would hide', Boolean(await evaluate(`__gm.locate('Hide 2 links')`)));
|
check('the port menu counts what it would hide', Boolean(await evaluate(`__gm.locate('Hide 2 links')`)));
|
||||||
await clickText('Hide 2 links');
|
await clickText('Hide 2 links');
|
||||||
await sleep(250);
|
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(200);
|
|
||||||
const ids = forked.map((e) => JSON.stringify(e.id));
|
const ids = forked.map((e) => JSON.stringify(e.id));
|
||||||
const markA = await json(`__gm.graph.foldMark(${ids[0]}, true)`);
|
const markA = await json(`__gm.graph.foldMark(${ids[0]}, true)`);
|
||||||
const markB = await json(`__gm.graph.foldMark(${ids[1]}, true)`);
|
const markB = await json(`__gm.graph.foldMark(${ids[1]}, true)`);
|
||||||
@@ -767,7 +723,6 @@ export async function graph(t) {
|
|||||||
// The bundled mark is still a handle: one click selects every far end, which is also what
|
// The bundled mark is still a handle: one click selects every far end, which is also what
|
||||||
// lends all of its curves back at once.
|
// lends all of its curves back at once.
|
||||||
await click(markA.x, markA.y);
|
await click(markA.x, markA.y);
|
||||||
await sleep(250);
|
|
||||||
const picked = await json('__gm.graph.selected()');
|
const picked = await json('__gm.graph.selected()');
|
||||||
check(
|
check(
|
||||||
'clicking a bundled mark selects every node the port feeds',
|
'clicking a bundled mark selects every node the port feeds',
|
||||||
@@ -781,15 +736,12 @@ export async function graph(t) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await click(markA.x, markA.y, 'right');
|
await click(markA.x, markA.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'the mark menu shows both links back in one go',
|
'the mark menu shows both links back in one go',
|
||||||
Boolean(await evaluate(`__gm.locate('Show 2 links')`)),
|
Boolean(await evaluate(`__gm.locate('Show 2 links')`)),
|
||||||
);
|
);
|
||||||
await clickText('Show 2 links');
|
await clickText('Show 2 links');
|
||||||
await sleep(250);
|
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(200);
|
|
||||||
const unfolded = (await asset('mover')).graph.edges.filter(
|
const unfolded = (await asset('mover')).graph.edges.filter(
|
||||||
(e) => e.from.node === edge.from.node && e.from.port === edge.from.port,
|
(e) => e.from.node === edge.from.node && e.from.port === edge.from.port,
|
||||||
);
|
);
|
||||||
@@ -801,7 +753,6 @@ export async function graph(t) {
|
|||||||
// One step of undo takes both back – hiding two links was one command, so it is one undo.
|
// One step of undo takes both back – hiding two links was one command, so it is one undo.
|
||||||
await undo();
|
await undo();
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(250);
|
|
||||||
const refolded = (await asset('mover')).graph.edges.filter(
|
const refolded = (await asset('mover')).graph.edges.filter(
|
||||||
(e) => e.from.node === edge.from.node && e.from.port === edge.from.port,
|
(e) => e.from.node === edge.from.node && e.from.port === edge.from.port,
|
||||||
);
|
);
|
||||||
@@ -811,7 +762,6 @@ export async function graph(t) {
|
|||||||
JSON.stringify(refolded.map((e) => e.folded)),
|
JSON.stringify(refolded.map((e) => e.folded)),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
const forkedNow = async () =>
|
const forkedNow = async () =>
|
||||||
(await asset('mover')).graph.edges.filter(
|
(await asset('mover')).graph.edges.filter(
|
||||||
@@ -821,13 +771,11 @@ export async function graph(t) {
|
|||||||
`__gm.graph.portAt(${JSON.stringify(edge.from.node)}, ${JSON.stringify(edge.from.port)}, true)`,
|
`__gm.graph.portAt(${JSON.stringify(edge.from.node)}, ${JSON.stringify(edge.from.port)}, true)`,
|
||||||
);
|
);
|
||||||
await click(portNow.x, portNow.y, 'right');
|
await click(portNow.x, portNow.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
const offered = await find('Disconnect 2 links');
|
const offered = await find('Disconnect 2 links');
|
||||||
if (offered) await click(offered.x, offered.y);
|
if (offered) await click(offered.x, offered.y);
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
const cut = await forkedNow();
|
const cut = await forkedNow();
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'disconnecting both links at a port is one undo step',
|
'disconnecting both links at a port is one undo step',
|
||||||
Boolean(offered) && cut === 0 && (await forkedNow()) === 2,
|
Boolean(offered) && cut === 0 && (await forkedNow()) === 2,
|
||||||
@@ -838,12 +786,10 @@ export async function graph(t) {
|
|||||||
// link lands on it until it is expanded, and the state is saved with the project.
|
// link lands on it until it is expanded, and the state is saved with the project.
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(250);
|
|
||||||
const edgesBefore13q = (await graphCounts()).edges;
|
const edgesBefore13q = (await graphCounts()).edges;
|
||||||
const openRect = await json(`__gm.graph.nodeRect('n-move')`);
|
const openRect = await json(`__gm.graph.nodeRect('n-move')`);
|
||||||
const chevron = await json(`__gm.graph.chevronAt('n-move')`);
|
const chevron = await json(`__gm.graph.chevronAt('n-move')`);
|
||||||
await click(chevron.x, chevron.y);
|
await click(chevron.x, chevron.y);
|
||||||
await sleep(200);
|
|
||||||
const shut = await json(`__gm.graph.nodeRect('n-move')`);
|
const shut = await json(`__gm.graph.nodeRect('n-move')`);
|
||||||
const zoom = openRect.w / 176;
|
const zoom = openRect.w / 176;
|
||||||
check(
|
check(
|
||||||
@@ -902,12 +848,9 @@ export async function graph(t) {
|
|||||||
for (const port of ['x', 'y']) {
|
for (const port of ['x', 'y']) {
|
||||||
const at = await json(`__gm.graph.portAt('n-axis', '${port}', true)`);
|
const at = await json(`__gm.graph.portAt('n-axis', '${port}', true)`);
|
||||||
await click(at.x, at.y, 'right');
|
await click(at.x, at.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
await clickText('Hide link');
|
await clickText('Hide link');
|
||||||
await sleep(200);
|
|
||||||
}
|
}
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await sleep(200);
|
|
||||||
const markDx = await json(`__gm.graph.foldMark('e3', false)`);
|
const markDx = await json(`__gm.graph.foldMark('e3', false)`);
|
||||||
const markDy = await json(`__gm.graph.foldMark('e4', false)`);
|
const markDy = await json(`__gm.graph.foldMark('e4', false)`);
|
||||||
check(
|
check(
|
||||||
@@ -919,9 +862,7 @@ export async function graph(t) {
|
|||||||
await undo();
|
await undo();
|
||||||
|
|
||||||
await select({ kind: 'node', assetId: 'mover', nodeId: 'n-move', nodeIds: ['n-speed', 'n-move'] });
|
await select({ kind: 'node', assetId: 'mover', nodeId: 'n-move', nodeIds: ['n-speed', 'n-move'] });
|
||||||
await sleep(150);
|
|
||||||
await click(title.x, title.y, 'right');
|
await click(title.x, title.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'a mixed selection is offered both ways',
|
'a mixed selection is offered both ways',
|
||||||
Boolean(await evaluate(`__gm.locate('Collapse node', false)`)) &&
|
Boolean(await evaluate(`__gm.locate('Collapse node', false)`)) &&
|
||||||
@@ -929,14 +870,11 @@ export async function graph(t) {
|
|||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(150);
|
|
||||||
check('undo expands it again', (await nodeOf('n-move')).collapsed === undefined);
|
check('undo expands it again', (await nodeOf('n-move')).collapsed === undefined);
|
||||||
|
|
||||||
await select({ kind: 'node', assetId: 'mover', nodeId: 'n-move', nodeIds: ['n-speed', 'n-move'] });
|
await select({ kind: 'node', assetId: 'mover', nodeId: 'n-move', nodeIds: ['n-speed', 'n-move'] });
|
||||||
await sleep(150);
|
|
||||||
const openTitle = await json(`__gm.graph.nodeAt('n-move')`);
|
const openTitle = await json(`__gm.graph.nodeAt('n-move')`);
|
||||||
await click(openTitle.x, openTitle.y, 'right');
|
await click(openTitle.x, openTitle.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
const collapseBoth = await json(`__gm.locate('Collapse 2 nodes', false)`);
|
const collapseBoth = await json(`__gm.locate('Collapse 2 nodes', false)`);
|
||||||
check('two expanded nodes are offered as one', collapseBoth !== null);
|
check('two expanded nodes are offered as one', collapseBoth !== null);
|
||||||
if (collapseBoth) await click(collapseBoth.x, collapseBoth.y);
|
if (collapseBoth) await click(collapseBoth.x, collapseBoth.y);
|
||||||
@@ -945,7 +883,6 @@ export async function graph(t) {
|
|||||||
const both = async () => [(await nodeOf('n-speed')).collapsed, (await nodeOf('n-move')).collapsed];
|
const both = async () => [(await nodeOf('n-speed')).collapsed, (await nodeOf('n-move')).collapsed];
|
||||||
check('the menu collapses the whole selection', JSON.stringify(await both()) === '[true,true]');
|
check('the menu collapses the whole selection', JSON.stringify(await both()) === '[true,true]');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'and one undo takes all of it back',
|
'and one undo takes all of it back',
|
||||||
JSON.stringify(await both()) === '[null,null]',
|
JSON.stringify(await both()) === '[null,null]',
|
||||||
@@ -957,16 +894,15 @@ export async function graph(t) {
|
|||||||
const again = await json(`__gm.graph.chevronAt('n-move')`);
|
const again = await json(`__gm.graph.chevronAt('n-move')`);
|
||||||
await click(again.x, again.y);
|
await click(again.x, again.y);
|
||||||
const savedMove = async () => {
|
const savedMove = async () => {
|
||||||
for (let i = 0; i < 30 && (await state()).save !== 'saved'; i++) await sleep(300);
|
await saved();
|
||||||
const saved = await api(`projects/${projectId}`);
|
const doc = await api(`projects/${projectId}`);
|
||||||
return saved.project.assets.find((a) => a.id === 'mover').graph.nodes.find((n) => n.id === 'n-move');
|
return doc.project.assets.find((a) => a.id === 'mover').graph.nodes.find((n) => n.id === 'n-move');
|
||||||
};
|
};
|
||||||
check('the collapse reaches the saved file', (await savedMove()).collapsed === true);
|
check('the collapse reaches the saved file', (await savedMove()).collapsed === true);
|
||||||
await navigate();
|
await navigate();
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
await select({ kind: 'asset', assetId: 'mover' });
|
await select({ kind: 'asset', assetId: 'mover' });
|
||||||
await evaluate(`__gm.showTab('graph')`);
|
await showTab('graph');
|
||||||
await sleep(250);
|
|
||||||
const reloaded = await json(`__gm.graph.nodeRect('n-move')`);
|
const reloaded = await json(`__gm.graph.nodeRect('n-move')`);
|
||||||
check(
|
check(
|
||||||
'and a reloaded project draws it collapsed again',
|
'and a reloaded project draws it collapsed again',
|
||||||
@@ -977,5 +913,5 @@ export async function graph(t) {
|
|||||||
await click(reopen.x, reopen.y);
|
await click(reopen.x, reopen.y);
|
||||||
check('expanding leaves no trace in the file', !('collapsed' in (await savedMove())));
|
check('expanding leaves no trace in the file', !('collapsed' in (await savedMove())));
|
||||||
|
|
||||||
await evaluate(`__gm.showTab('scene')`);
|
await showTab('scene');
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,6 @@ import { same } from '../lib/harness.mjs';
|
|||||||
export async function parenting(t) {
|
export async function parenting(t) {
|
||||||
const {
|
const {
|
||||||
check,
|
check,
|
||||||
sleep,
|
|
||||||
json,
|
json,
|
||||||
shot,
|
shot,
|
||||||
click,
|
click,
|
||||||
@@ -36,7 +35,6 @@ export async function parenting(t) {
|
|||||||
const redRow = await locate('Red crate');
|
const redRow = await locate('Red crate');
|
||||||
const blueRow = await locate('Blue crate');
|
const blueRow = await locate('Blue crate');
|
||||||
await drag(blueRow.x, blueRow.y, redRow.x, redRow.y);
|
await drag(blueRow.x, blueRow.y, redRow.x, redRow.y);
|
||||||
await sleep(100);
|
|
||||||
let o2 = await object('o2');
|
let o2 = await object('o2');
|
||||||
check(
|
check(
|
||||||
'dropping a row onto another parents it, keeping the world position',
|
'dropping a row onto another parents it, keeping the world position',
|
||||||
@@ -60,7 +58,6 @@ export async function parenting(t) {
|
|||||||
await press('a', { ctrl: true });
|
await press('a', { ctrl: true });
|
||||||
await typeText('-50');
|
await typeText('-50');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
o2 = await object('o2');
|
o2 = await object('o2');
|
||||||
const parent = await object('o2').then((o) => object(o.parentId));
|
const parent = await object('o2').then((o) => object(o.parentId));
|
||||||
const worldOfO2 = { x: o2.position.x + parent.position.x, y: o2.position.y + parent.position.y };
|
const worldOfO2 = { x: o2.position.x + parent.position.x, y: o2.position.y + parent.position.y };
|
||||||
@@ -73,7 +70,6 @@ export async function parenting(t) {
|
|||||||
// 7c. Play: entities mirror the hierarchy; undo restores parent and position.
|
// 7c. Play: entities mirror the hierarchy; undo restores parent and position.
|
||||||
await select(null);
|
await select(null);
|
||||||
await press('Enter', { ctrl: true });
|
await press('Enter', { ctrl: true });
|
||||||
await sleep(300);
|
|
||||||
const e2 = await entity('o2');
|
const e2 = await entity('o2');
|
||||||
check(
|
check(
|
||||||
'play instantiates the parent link and world positions',
|
'play instantiates the parent link and world positions',
|
||||||
@@ -100,7 +96,6 @@ export async function parenting(t) {
|
|||||||
JSON.stringify(entries),
|
JSON.stringify(entries),
|
||||||
);
|
);
|
||||||
await typeText('red');
|
await typeText('red');
|
||||||
await sleep(150);
|
|
||||||
const filtered = await json('__gm.overlayTexts()');
|
const filtered = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'typing filters the picker down to the matching entries',
|
'typing filters the picker down to the matching entries',
|
||||||
@@ -109,7 +104,6 @@ export async function parenting(t) {
|
|||||||
);
|
);
|
||||||
await shot('30-parent-search');
|
await shot('30-parent-search');
|
||||||
await press('Enter'); // takes the first match
|
await press('Enter'); // takes the first match
|
||||||
await sleep(150);
|
|
||||||
o2 = await object('o2');
|
o2 = await object('o2');
|
||||||
check(
|
check(
|
||||||
'inspector Parent menu parents the object',
|
'inspector Parent menu parents the object',
|
||||||
@@ -120,7 +114,6 @@ export async function parenting(t) {
|
|||||||
const blueRow2 = await locate('Blue crate');
|
const blueRow2 = await locate('Blue crate');
|
||||||
await click(blueRow2.x, blueRow2.y, 'right');
|
await click(blueRow2.x, blueRow2.y, 'right');
|
||||||
await clickText('Unparent (move to top level)');
|
await clickText('Unparent (move to top level)');
|
||||||
await sleep(100);
|
|
||||||
o2 = await object('o2');
|
o2 = await object('o2');
|
||||||
check(
|
check(
|
||||||
'Unparent moves the object to the top level, keeping its world position',
|
'Unparent moves the object to the top level, keeping its world position',
|
||||||
@@ -140,7 +133,6 @@ export async function parenting(t) {
|
|||||||
const blueRow3 = await locate('Blue crate');
|
const blueRow3 = await locate('Blue crate');
|
||||||
// 10 px above the centre of a 26 px row is its top quarter – the gap, not the row.
|
// 10 px above the centre of a 26 px row is its top quarter – the gap, not the row.
|
||||||
await drag(blueRow3.x, blueRow3.y, redRow3.x, redRow3.y - 10);
|
await drag(blueRow3.x, blueRow3.y, redRow3.x, redRow3.y - 10);
|
||||||
await sleep(150);
|
|
||||||
const rowsAfter = await json('__gm.hierarchyOrder()');
|
const rowsAfter = await json('__gm.hierarchyOrder()');
|
||||||
o2 = await object('o2');
|
o2 = await object('o2');
|
||||||
check(
|
check(
|
||||||
@@ -155,7 +147,6 @@ export async function parenting(t) {
|
|||||||
);
|
);
|
||||||
await shot('31-reordered');
|
await shot('31-reordered');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'undo puts the order back',
|
'undo puts the order back',
|
||||||
same(await json('__gm.hierarchyOrder()'), rowsBefore),
|
same(await json('__gm.hierarchyOrder()'), rowsBefore),
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export async function colliders(t) {
|
|||||||
const redRow = await locate('Red crate');
|
const redRow = await locate('Red crate');
|
||||||
await click(redRow.x, redRow.y, 'right');
|
await click(redRow.x, redRow.y, 'right');
|
||||||
await clickText('Add collider');
|
await clickText('Add collider');
|
||||||
await sleep(100);
|
|
||||||
const s = await state();
|
const s = await state();
|
||||||
const colObj = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;
|
const colObj = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;
|
||||||
const colAsset = colObj ? await asset(colObj.assetId) : null;
|
const colAsset = colObj ? await asset(colObj.assetId) : null;
|
||||||
@@ -102,7 +101,6 @@ export async function colliders(t) {
|
|||||||
${speed ? `__gm.state.project.assets.find(a => a.id === 'mover').props.speed = ${speed};` : ''}
|
${speed ? `__gm.state.project.assets.find(a => a.id === 'mover').props.speed = ${speed};` : ''}
|
||||||
__gm.state.select(null)`);
|
__gm.state.select(null)`);
|
||||||
await press('Enter', { ctrl: true });
|
await press('Enter', { ctrl: true });
|
||||||
await sleep(300);
|
|
||||||
await holdKey('ArrowRight', 700);
|
await holdKey('ArrowRight', 700);
|
||||||
const red = await entity('o1');
|
const red = await entity('o1');
|
||||||
await stop();
|
await stop();
|
||||||
@@ -128,6 +126,5 @@ export async function colliders(t) {
|
|||||||
);
|
);
|
||||||
await mutate(`__gm.state.project.assets.find(a => a.id === 'mover').props.speed = 200`); // later steps expect the seeded speed
|
await mutate(`__gm.state.project.assets.find(a => a.id === 'mover').props.speed = 200`); // later steps expect the seeded speed
|
||||||
await select({ kind: 'object', objectId: 'oc2' });
|
await select({ kind: 'object', objectId: 'oc2' });
|
||||||
await sleep(200);
|
|
||||||
await shot('22-trigger-collider');
|
await shot('22-trigger-collider');
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import { same } from '../lib/harness.mjs';
|
|||||||
*/
|
*/
|
||||||
export async function containers(t) {
|
export async function containers(t) {
|
||||||
const {
|
const {
|
||||||
|
saved,
|
||||||
check,
|
check,
|
||||||
sleep,
|
sleep,
|
||||||
evaluate,
|
evaluate,
|
||||||
@@ -44,7 +45,6 @@ export async function containers(t) {
|
|||||||
await press('a', { ctrl: true });
|
await press('a', { ctrl: true });
|
||||||
await typeText(String(value));
|
await typeText(String(value));
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
const notice = async () => {
|
const notice = async () => {
|
||||||
await evaluate("__gm.state.notice = ''");
|
await evaluate("__gm.state.notice = ''");
|
||||||
@@ -52,7 +52,6 @@ export async function containers(t) {
|
|||||||
};
|
};
|
||||||
const selectObject = async (id) => {
|
const selectObject = async (id) => {
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: id, objectIds: [id] });
|
await select({ kind: 'object', holderId: 'scene1', objectId: id, objectIds: [id] });
|
||||||
await sleep(250);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 9a. Reset both crates to known spots, then Ctrl-click the second row in the hierarchy:
|
// 9a. Reset both crates to known spots, then Ctrl-click the second row in the hierarchy:
|
||||||
@@ -116,7 +115,6 @@ export async function containers(t) {
|
|||||||
const listedBeforeDelete = (await objects()).length;
|
const listedBeforeDelete = (await objects()).length;
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o2', objectIds: ['o1', 'o2'] });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'o2', objectIds: ['o1', 'o2'] });
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'Delete removes the whole multi-selection with its subtrees',
|
'Delete removes the whole multi-selection with its subtrees',
|
||||||
(await objects()).length === listedBeforeDelete - 5,
|
(await objects()).length === listedBeforeDelete - 5,
|
||||||
@@ -131,7 +129,6 @@ export async function containers(t) {
|
|||||||
const listedBefore = (await objects()).length;
|
const listedBefore = (await objects()).length;
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o2', objectIds: ['o1', 'o2'] });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'o2', objectIds: ['o1', 'o2'] });
|
||||||
await clickText('Create container');
|
await clickText('Create container');
|
||||||
await sleep(150);
|
|
||||||
s = await state();
|
s = await state();
|
||||||
const instanceA = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;
|
const instanceA = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;
|
||||||
const containerAsset = instanceA ? await asset(instanceA.assetId) : null;
|
const containerAsset = instanceA ? await asset(instanceA.assetId) : null;
|
||||||
@@ -304,11 +301,8 @@ export async function containers(t) {
|
|||||||
const before = (await objects()).length;
|
const before = (await objects()).length;
|
||||||
const noticeNow = await notice();
|
const noticeNow = await notice();
|
||||||
await click(row.x, row.y, 'right');
|
await click(row.x, row.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
await clickText('Add collider');
|
await clickText('Add collider');
|
||||||
await sleep(200);
|
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(100);
|
|
||||||
return { added: (await objects()).length - before, notice: await noticeNow() };
|
return { added: (await objects()).length - before, notice: await noticeNow() };
|
||||||
};
|
};
|
||||||
const refused = JSON.stringify({ added: 0, notice: '' });
|
const refused = JSON.stringify({ added: 0, notice: '' });
|
||||||
@@ -340,11 +334,9 @@ export async function containers(t) {
|
|||||||
JSON.stringify(fromPlain),
|
JSON.stringify(fromPlain),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
await selectObject(redA);
|
await selectObject(redA);
|
||||||
let noticeAfter = await notice();
|
let noticeAfter = await notice();
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'removing resolved content is refused with a notice',
|
'removing resolved content is refused with a notice',
|
||||||
(await noticeAfter()).includes('container template') && Boolean(await object(redA)),
|
(await noticeAfter()).includes('container template') && Boolean(await object(redA)),
|
||||||
@@ -352,7 +344,6 @@ export async function containers(t) {
|
|||||||
);
|
);
|
||||||
noticeAfter = await notice();
|
noticeAfter = await notice();
|
||||||
await press('g', { ctrl: true });
|
await press('g', { ctrl: true });
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'making a container out of resolved content is refused too',
|
'making a container out of resolved content is refused too',
|
||||||
(await noticeAfter()).includes('container template'),
|
(await noticeAfter()).includes('container template'),
|
||||||
@@ -360,7 +351,6 @@ export async function containers(t) {
|
|||||||
);
|
);
|
||||||
const parentRow = await json("__gm.inspectorField('Parent')");
|
const parentRow = await json("__gm.inspectorField('Parent')");
|
||||||
await click(parentRow.x, parentRow.y);
|
await click(parentRow.x, parentRow.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'the Parent picker does not even open for resolved content',
|
'the Parent picker does not even open for resolved content',
|
||||||
same(await json('__gm.overlayTexts()'), []),
|
same(await json('__gm.overlayTexts()'), []),
|
||||||
@@ -377,7 +367,6 @@ export async function containers(t) {
|
|||||||
const redARow = await rowAt(redA);
|
const redARow = await rowAt(redA);
|
||||||
const otherRow = await rowAt(instanceB.id);
|
const otherRow = await rowAt(instanceB.id);
|
||||||
await drag(redARow.x, redARow.y, otherRow.x, otherRow.y);
|
await drag(redARow.x, redARow.y, otherRow.x, otherRow.y);
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
're-parenting resolved content by dragging its row is refused',
|
're-parenting resolved content by dragging its row is refused',
|
||||||
(await noticeAfter()).includes('container template') && (await object(redA)).parentId === instanceA.id,
|
(await noticeAfter()).includes('container template') && (await object(redA)).parentId === instanceA.id,
|
||||||
@@ -400,7 +389,6 @@ export async function containers(t) {
|
|||||||
await has('Follows a template of 5 object(s).'),
|
await has('Follows a template of 5 object(s).'),
|
||||||
);
|
);
|
||||||
await clickText('Unpack');
|
await clickText('Unpack');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'Unpack writes the content into the scene, keeping the ids and the listing',
|
'Unpack writes the content into the scene, keeping the ids and the listing',
|
||||||
(await storedObjects()).length === storedBeforeUnpack + 5 &&
|
(await storedObjects()).length === storedBeforeUnpack + 5 &&
|
||||||
@@ -429,19 +417,16 @@ export async function containers(t) {
|
|||||||
// not freeze the picker any more – the inspector has to read the holder, not the id.
|
// not freeze the picker any more – the inspector has to read the holder, not the id.
|
||||||
const unpackedParentRow = await json("__gm.inspectorField('Parent')");
|
const unpackedParentRow = await json("__gm.inspectorField('Parent')");
|
||||||
await click(unpackedParentRow.x, unpackedParentRow.y);
|
await click(unpackedParentRow.x, unpackedParentRow.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'unpacked content offers the Parent picker like any ordinary object',
|
'unpacked content offers the Parent picker like any ordinary object',
|
||||||
(await json('__gm.overlayTexts()')).includes('None'),
|
(await json('__gm.overlayTexts()')).includes('None'),
|
||||||
JSON.stringify(await json('__gm.overlayTexts()')),
|
JSON.stringify(await json('__gm.overlayTexts()')),
|
||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(100);
|
|
||||||
noticeAfter = await notice();
|
noticeAfter = await notice();
|
||||||
const redCRow = await rowAt(redC);
|
const redCRow = await rowAt(redC);
|
||||||
await click(redCRow.x, redCRow.y, 'right');
|
await click(redCRow.x, redCRow.y, 'right');
|
||||||
await clickText('Unparent (move to top level)');
|
await clickText('Unparent (move to top level)');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'unpacked content can be re-parented, which was refused while it followed the template',
|
'unpacked content can be re-parented, which was refused while it followed the template',
|
||||||
(await object(redC)).parentId === '' && (await noticeAfter()) === '',
|
(await object(redC)).parentId === '' && (await noticeAfter()) === '',
|
||||||
@@ -450,7 +435,6 @@ export async function containers(t) {
|
|||||||
noticeAfter = await notice();
|
noticeAfter = await notice();
|
||||||
await selectObject(redC);
|
await selectObject(redC);
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'and it can be removed – the refusal was about following a template, not about the id',
|
'and it can be removed – the refusal was about following a template, not about the id',
|
||||||
!(await object(redC)) && (await noticeAfter()) === '',
|
!(await object(redC)) && (await noticeAfter()) === '',
|
||||||
@@ -458,7 +442,6 @@ export async function containers(t) {
|
|||||||
);
|
);
|
||||||
await selectObject(instanceC.id);
|
await selectObject(instanceC.id);
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'removing the unpacked container takes the rest of its content with it',
|
'removing the unpacked container takes the rest of its content with it',
|
||||||
(await storedObjects()).length === storedBeforeUnpack - 1,
|
(await storedObjects()).length === storedBeforeUnpack - 1,
|
||||||
@@ -475,7 +458,6 @@ export async function containers(t) {
|
|||||||
const beforeSpawn = await objects();
|
const beforeSpawn = await objects();
|
||||||
await play();
|
await play();
|
||||||
await press('Space');
|
await press('Space');
|
||||||
await sleep(250);
|
|
||||||
const fresh = (await objects()).filter((o) => !beforeSpawn.some((b) => b.id === o.id));
|
const fresh = (await objects()).filter((o) => !beforeSpawn.some((b) => b.id === o.id));
|
||||||
const spawnedRoot = fresh.find((o) => o.parentId === '');
|
const spawnedRoot = fresh.find((o) => o.parentId === '');
|
||||||
check(
|
check(
|
||||||
@@ -506,7 +488,6 @@ export async function containers(t) {
|
|||||||
check('a moved instance shows apply and revert on its X row', Boolean(applyButton) && (await has('↺')));
|
check('a moved instance shows apply and revert on its X row', Boolean(applyButton) && (await has('↺')));
|
||||||
await shot('27-apply-buttons');
|
await shot('27-apply-buttons');
|
||||||
await click(applyButton.x, applyButton.y);
|
await click(applyButton.x, applyButton.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'apply writes x into the template and the untouched instance follows',
|
'apply writes x into the template and the untouched instance follows',
|
||||||
(await templateRed()).position.x === -100 && (await object(redA)).position.x === -100,
|
(await templateRed()).position.x === -100 && (await object(redA)).position.x === -100,
|
||||||
@@ -520,7 +501,6 @@ export async function containers(t) {
|
|||||||
);
|
);
|
||||||
const revertButton = await evaluate(`__gm.locate('↺')`);
|
const revertButton = await evaluate(`__gm.locate('↺')`);
|
||||||
await click(revertButton.x, revertButton.y);
|
await click(revertButton.x, revertButton.y);
|
||||||
await sleep(150);
|
|
||||||
const revertedB = (await storedObjects()).find((o) => o.id === instanceB.id);
|
const revertedB = (await storedObjects()).find((o) => o.id === instanceB.id);
|
||||||
check(
|
check(
|
||||||
'revert drops the deviation, so the instance follows the template again',
|
'revert drops the deviation, so the instance follows the template again',
|
||||||
@@ -554,7 +534,6 @@ export async function containers(t) {
|
|||||||
);
|
);
|
||||||
await shot('28-apply-menu');
|
await shot('28-apply-menu');
|
||||||
await clickText('Apply to asset');
|
await clickText('Apply to asset');
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'Apply to asset writes the value into the asset and drops the deviation',
|
'Apply to asset writes the value into the asset and drops the deviation',
|
||||||
(await asset('crate')).props.width === 77 && (await object(redB)).overrides.width === undefined,
|
(await asset('crate')).props.width === 77 && (await object(redB)).overrides.width === undefined,
|
||||||
@@ -567,7 +546,6 @@ export async function containers(t) {
|
|||||||
);
|
);
|
||||||
await click(applyMenu.x, applyMenu.y);
|
await click(applyMenu.x, applyMenu.y);
|
||||||
await clickText('Apply to container');
|
await clickText('Apply to container');
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'Apply to container writes the override into the template, so every instance shows it',
|
'Apply to container writes the override into the template, so every instance shows it',
|
||||||
(await templateRed()).overrides.width === 77 && (await object(redA)).overrides.width === 77,
|
(await templateRed()).overrides.width === 77 && (await object(redA)).overrides.width === 77,
|
||||||
@@ -597,7 +575,7 @@ export async function containers(t) {
|
|||||||
// The content must be nowhere in the saved scene, and reloading must resolve it back.
|
// The content must be nowhere in the saved scene, and reloading must resolve it back.
|
||||||
await selectObject(redB);
|
await selectObject(redB);
|
||||||
await setField('Y', 12);
|
await setField('Y', 12);
|
||||||
for (let i = 0; i < 30 && (await state()).save !== 'saved'; i++) await sleep(300);
|
await saved();
|
||||||
const savedScene = sceneOf(await api(`projects/${projectId}`));
|
const savedScene = sceneOf(await api(`projects/${projectId}`));
|
||||||
const savedInstance = savedScene.objects.find((o) => o.id === instanceB.id);
|
const savedInstance = savedScene.objects.find((o) => o.id === instanceB.id);
|
||||||
check(
|
check(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
export async function textAndEnabled(t) {
|
export async function textAndEnabled(t) {
|
||||||
const {
|
const {
|
||||||
check,
|
check,
|
||||||
sleep,
|
|
||||||
evaluate,
|
evaluate,
|
||||||
json,
|
json,
|
||||||
has,
|
has,
|
||||||
@@ -21,7 +20,6 @@ export async function textAndEnabled(t) {
|
|||||||
// 10a. Text assets: created through the + menu, placed, and rendered as real text in the scene.
|
// 10a. Text assets: created through the + menu, placed, and rendered as real text in the scene.
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await clickText('Text');
|
await clickText('Text');
|
||||||
await sleep(100);
|
|
||||||
const textAssetId = await evaluate(`(__gm.state.project.assets.find(a => a.type === 'text') || {}).id`);
|
const textAssetId = await evaluate(`(__gm.state.project.assets.find(a => a.type === 'text') || {}).id`);
|
||||||
check('+ menu creates a text asset', Boolean(textAssetId));
|
check('+ menu creates a text asset', Boolean(textAssetId));
|
||||||
await mutate(`
|
await mutate(`
|
||||||
@@ -31,7 +29,6 @@ export async function textAndEnabled(t) {
|
|||||||
const cam = __gm.state.activeScene.objects.find(o => o.id === 'o3').position;
|
const cam = __gm.state.activeScene.objects.find(o => o.id === 'o3').position;
|
||||||
__gm.state.activeScene.objects.push({ id: 'otext', assetId: id, name: '', position: { x: cam.x, y: cam.y, z: 5 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: '', instanceEdits: {} });
|
__gm.state.activeScene.objects.push({ id: 'otext', assetId: id, name: '', position: { x: cam.x, y: cam.y, z: 5 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: '', instanceEdits: {} });
|
||||||
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
||||||
await sleep(100);
|
|
||||||
check('a placed text object draws its content in the scene', await has('Hello world'));
|
check('a placed text object draws its content in the scene', await has('Hello world'));
|
||||||
await shot('29-text-object');
|
await shot('29-text-object');
|
||||||
|
|
||||||
@@ -44,7 +41,6 @@ export async function textAndEnabled(t) {
|
|||||||
const gizmoOn = await gizmoOf('otext');
|
const gizmoOn = await gizmoOf('otext');
|
||||||
check('a selected object is framed at full strength', gizmoOn?.alpha === 1, JSON.stringify(gizmoOn));
|
check('a selected object is framed at full strength', gizmoOn?.alpha === 1, JSON.stringify(gizmoOn));
|
||||||
await click(enabledRow.x, enabledRow.y);
|
await click(enabledRow.x, enabledRow.y);
|
||||||
await sleep(100);
|
|
||||||
const textObject = await object('otext');
|
const textObject = await object('otext');
|
||||||
check(
|
check(
|
||||||
'clicking it writes an override instead of changing the asset',
|
'clicking it writes an override instead of changing the asset',
|
||||||
@@ -75,11 +71,9 @@ export async function textAndEnabled(t) {
|
|||||||
);
|
);
|
||||||
const otherRow = await evaluate(`__gm.hierarchyRow('o3')`);
|
const otherRow = await evaluate(`__gm.hierarchyRow('o3')`);
|
||||||
await click(otherRow.x, otherRow.y);
|
await click(otherRow.x, otherRow.y);
|
||||||
await sleep(100);
|
|
||||||
check('selecting something else drops the frame', (await gizmoOf('otext')) === null);
|
check('selecting something else drops the frame', (await gizmoOf('otext')) === null);
|
||||||
const offRow = await evaluate(`__gm.hierarchyRow('otext')`);
|
const offRow = await evaluate(`__gm.hierarchyRow('otext')`);
|
||||||
await click(offRow.x, offRow.y);
|
await click(offRow.x, offRow.y);
|
||||||
await sleep(100);
|
|
||||||
const gizmoBack = await gizmoOf('otext');
|
const gizmoBack = await gizmoOf('otext');
|
||||||
check(
|
check(
|
||||||
'picking the switched-off object in the hierarchy frames it again',
|
'picking the switched-off object in the hierarchy frames it again',
|
||||||
@@ -92,10 +86,8 @@ export async function textAndEnabled(t) {
|
|||||||
// 10c. In the running game a switched-off object does not exist: it is not rendered, and
|
// 10c. In the running game a switched-off object does not exist: it is not rendered, and
|
||||||
// switching it back on brings it back.
|
// switching it back on brings it back.
|
||||||
await play();
|
await play();
|
||||||
await sleep(200);
|
|
||||||
const offItems = (await json('__gm.gameInfo()')).visibleItems;
|
const offItems = (await json('__gm.gameInfo()')).visibleItems;
|
||||||
await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'otext').overrides.enabled = true`);
|
await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'otext').overrides.enabled = true`);
|
||||||
await sleep(200);
|
|
||||||
const onItems = (await json('__gm.gameInfo()')).visibleItems;
|
const onItems = (await json('__gm.gameInfo()')).visibleItems;
|
||||||
check(
|
check(
|
||||||
'the game renders one item more once the object is switched on',
|
'the game renders one item more once the object is switched on',
|
||||||
@@ -112,7 +104,6 @@ export async function textAndEnabled(t) {
|
|||||||
await click(fontLabel.x + 160, fontLabel.y);
|
await click(fontLabel.x + 160, fontLabel.y);
|
||||||
await typeText('32.6');
|
await typeText('32.6');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
const fontSize = (await object('otext')).overrides.fontSize;
|
const fontSize = (await object('otext')).overrides.fontSize;
|
||||||
check('typing a fraction into it rounds the stored value', fontSize === 33, String(fontSize));
|
check('typing a fraction into it rounds the stored value', fontSize === 33, String(fontSize));
|
||||||
check('and the field shows exactly what was stored', Boolean(await has('33')));
|
check('and the field shows exactly what was stored', Boolean(await has('33')));
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export async function menus(t) {
|
|||||||
scene.objects.push(copy);
|
scene.objects.push(copy);
|
||||||
}
|
}
|
||||||
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
// 11a. A picker with 40+ entries is capped in height and stays fully on screen instead of
|
// 11a. A picker with 40+ entries is capped in height and stays fully on screen instead of
|
||||||
// running off the bottom edge. The cap is a share of the canvas, not a constant: a tenth is
|
// running off the bottom edge. The cap is a share of the canvas, not a constant: a tenth is
|
||||||
|
|||||||
@@ -244,9 +244,7 @@ export async function textures(t) {
|
|||||||
const sprite = __gm.state.project.assets.find((a) => a.id === 'tex-sprite');
|
const sprite = __gm.state.project.assets.find((a) => a.id === 'tex-sprite');
|
||||||
sprite.props.shader = ''; delete sprite.props.brightness;
|
sprite.props.shader = ''; delete sprite.props.brightness;
|
||||||
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(texture.id)}).props.filter = 'linear'`);
|
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(texture.id)}).props.filter = 'linear'`);
|
||||||
await sleep(400);
|
|
||||||
await mutate(enlarge(160, 120));
|
await mutate(enlarge(160, 120));
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 12d. The colour multiplies the picture, so a tinted white quadrant stops being white.
|
// 12d. The colour multiplies the picture, so a tinted white quadrant stops being white.
|
||||||
await mutate("__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.color = 0x3366ff");
|
await mutate("__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.color = 0x3366ff");
|
||||||
@@ -282,7 +280,6 @@ export async function textures(t) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
const afterUndo = await json(
|
const afterUndo = await json(
|
||||||
"__gm.state.project.assets.filter((a) => a.type === 'texture').map((a) => a.file)",
|
"__gm.state.project.assets.filter((a) => a.type === 'texture').map((a) => a.file)",
|
||||||
);
|
);
|
||||||
@@ -407,7 +404,6 @@ export async function textures(t) {
|
|||||||
|
|
||||||
// Back to one texture asset, the way the later steps expect the fixture.
|
// Back to one texture asset, the way the later steps expect the fixture.
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'undo takes the adopted asset back out',
|
'undo takes the adopted asset back out',
|
||||||
(await json("__gm.state.project.assets.filter((a) => a.type === 'texture').length")) === 1,
|
(await json("__gm.state.project.assets.filter((a) => a.type === 'texture').length")) === 1,
|
||||||
@@ -417,23 +413,19 @@ export async function textures(t) {
|
|||||||
const entryText = 'Create sprite from texture';
|
const entryText = 'Create sprite from texture';
|
||||||
const spriteRow = await json("__gm.assetRow('crate')");
|
const spriteRow = await json("__gm.assetRow('crate')");
|
||||||
await click(spriteRow.x, spriteRow.y, 'right');
|
await click(spriteRow.x, spriteRow.y, 'right');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'a sprite asset does not offer making a sprite from it',
|
'a sprite asset does not offer making a sprite from it',
|
||||||
(await json(`__gm.locate(${JSON.stringify(entryText)}, false)`)) === null,
|
(await json(`__gm.locate(${JSON.stringify(entryText)}, false)`)) === null,
|
||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
const spritesBefore = await json("__gm.state.project.assets.filter((a) => a.type === 'sprite').length");
|
const spritesBefore = await json("__gm.state.project.assets.filter((a) => a.type === 'sprite').length");
|
||||||
const textureRow = await json(`__gm.assetRow(${JSON.stringify(texture.id)})`);
|
const textureRow = await json(`__gm.assetRow(${JSON.stringify(texture.id)})`);
|
||||||
await click(textureRow.x, textureRow.y, 'right');
|
await click(textureRow.x, textureRow.y, 'right');
|
||||||
await sleep(200);
|
|
||||||
const createEntry = await json(`__gm.locate(${JSON.stringify(entryText)}, false)`);
|
const createEntry = await json(`__gm.locate(${JSON.stringify(entryText)}, false)`);
|
||||||
check('a texture asset offers making a sprite from it', createEntry !== null);
|
check('a texture asset offers making a sprite from it', createEntry !== null);
|
||||||
if (createEntry) {
|
if (createEntry) {
|
||||||
await click(createEntry.x, createEntry.y);
|
await click(createEntry.x, createEntry.y);
|
||||||
await sleep(400);
|
|
||||||
const made = await json(`(() => {
|
const made = await json(`(() => {
|
||||||
const s = __gm.state.selection;
|
const s = __gm.state.selection;
|
||||||
const a = s && s.kind === 'asset' ? __gm.state.project.assets.find((x) => x.id === s.assetId) : null;
|
const a = s && s.kind === 'asset' ? __gm.state.project.assets.find((x) => x.id === s.assetId) : null;
|
||||||
|
|||||||
+5
-11
@@ -35,7 +35,7 @@ function nearestColour(sample) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function frames(t) {
|
export async function frames(t) {
|
||||||
const { check, sleep, json, evaluate, shot, clickText, drag, press, undo, wheel } = t;
|
const { check, sleep, json, evaluate, shot, clickText, drag, press, undo, wheel, showTab } = t;
|
||||||
|
|
||||||
// 13a. The tab exists and shows the texture the selection points at.
|
// 13a. The tab exists and shows the texture the selection points at.
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
@@ -43,7 +43,6 @@ export async function frames(t) {
|
|||||||
const texture = await json("(__gm.state.project.assets.find((a) => a.type === 'texture') ?? {}).id ?? ''");
|
const texture = await json("(__gm.state.project.assets.find((a) => a.type === 'texture') ?? {}).id ?? ''");
|
||||||
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(texture)} })`);
|
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(texture)} })`);
|
||||||
await openTab(t, 'texture');
|
await openTab(t, 'texture');
|
||||||
await sleep(400);
|
|
||||||
check('the Texture tab opens', (await json('__gm.tab()')) === 'texture');
|
check('the Texture tab opens', (await json('__gm.tab()')) === 'texture');
|
||||||
check(
|
check(
|
||||||
'and shows the selected sheet',
|
'and shows the selected sheet',
|
||||||
@@ -84,7 +83,6 @@ export async function frames(t) {
|
|||||||
|
|
||||||
// 13c. It is one undo step, not one per pointer move.
|
// 13c. It is one undo step, not one per pointer move.
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'undo removes the frame in one step',
|
'undo removes the frame in one step',
|
||||||
(await json(
|
(await json(
|
||||||
@@ -92,7 +90,6 @@ export async function frames(t) {
|
|||||||
)) === 0,
|
)) === 0,
|
||||||
);
|
);
|
||||||
await t.redo();
|
await t.redo();
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 13d. A sprite wearing the frame shows that part of the sheet and nothing else.
|
// 13d. A sprite wearing the frame shows that part of the sheet and nothing else.
|
||||||
await evaluate(`(() => {
|
await evaluate(`(() => {
|
||||||
@@ -101,7 +98,7 @@ export async function frames(t) {
|
|||||||
sprite.props.texture = ${JSON.stringify(frame.id)};
|
sprite.props.texture = ${JSON.stringify(frame.id)};
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
})()`);
|
})()`);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(900);
|
await sleep(900);
|
||||||
|
|
||||||
const box = await json("__gm.objectRect('otex')");
|
const box = await json("__gm.objectRect('otex')");
|
||||||
@@ -128,8 +125,7 @@ export async function frames(t) {
|
|||||||
__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.color = 0xff00ff;
|
__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.color = 0xff00ff;
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
})()`);
|
})()`);
|
||||||
await evaluate("__gm.showTab('texture')");
|
await showTab('texture');
|
||||||
await sleep(400);
|
|
||||||
await clickText('Slice…');
|
await clickText('Slice…');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const dialog = await json('__gm.overlayTexts()');
|
const dialog = await json('__gm.overlayTexts()');
|
||||||
@@ -160,8 +156,7 @@ export async function frames(t) {
|
|||||||
// 13f. Zoomed in, the sheet is larger than the tab: nothing of it may be drawn next to the tab.
|
// 13f. Zoomed in, the sheet is larger than the tab: nothing of it may be drawn next to the tab.
|
||||||
// The bug this guards against drew the checkerboard's base rectangle, the frame outlines
|
// The bug this guards against drew the checkerboard's base rectangle, the frame outlines
|
||||||
// and the name plates over the Assets panel, because only the picture itself was masked.
|
// and the name plates over the Assets panel, because only the picture itself was masked.
|
||||||
await evaluate("__gm.showTab('texture')");
|
await showTab('texture');
|
||||||
await sleep(400);
|
|
||||||
const area = await json('__gm.texture.rect()');
|
const area = await json('__gm.texture.rect()');
|
||||||
// Points on the panel left of the tab and on the tab row above it – both were painted over.
|
// Points on the panel left of the tab and on the tab row above it – both were painted over.
|
||||||
const probes = [
|
const probes = [
|
||||||
@@ -198,10 +193,9 @@ export async function frames(t) {
|
|||||||
JSON.stringify(await json('__gm.textOverflows()')),
|
JSON.stringify(await json('__gm.textOverflows()')),
|
||||||
);
|
);
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 13g. A sprite pointed at a frame slicing removed falls back to its colour.
|
// 13g. A sprite pointed at a frame slicing removed falls back to its colour.
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(700);
|
await sleep(700);
|
||||||
if (box) {
|
if (box) {
|
||||||
const fallback = await json(
|
const fallback = await json(
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import { TAB_LABELS } from './lib/scaffold.mjs';
|
|||||||
const LONG_NAME = 'Player movement with jumping and coyote time';
|
const LONG_NAME = 'Player movement with jumping and coyote time';
|
||||||
|
|
||||||
export async function overflow(t) {
|
export async function overflow(t) {
|
||||||
const { check, sleep, json, evaluate, shot, mutate, drag, clickText } = t;
|
const { check, sleep, json, evaluate, shot, mutate, drag, clickText, showTab } = t;
|
||||||
|
|
||||||
const pickerWidths = async () => {
|
const pickerWidths = async () => {
|
||||||
const out = {};
|
const out = {};
|
||||||
for (const tab of ['graph', 'texture', 'container']) {
|
for (const tab of ['graph', 'texture', 'container']) {
|
||||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
await showTab(tab);
|
||||||
await sleep(300);
|
|
||||||
out[tab] = await json(`__gm.${tab}.pickerWidth()`);
|
out[tab] = await json(`__gm.${tab}.pickerWidth()`);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -42,8 +41,7 @@ export async function overflow(t) {
|
|||||||
|
|
||||||
// 14b. The tab row itself: a button past the centre column's edge sits under the inspector, and
|
// 14b. The tab row itself: a button past the centre column's edge sits under the inspector, and
|
||||||
// a click on it lands there (nine tabs on a 90 px floor did exactly that at 1280 px wide).
|
// a click on it lands there (nine tabs on a 90 px floor did exactly that at 1280 px wide).
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
const columnRight = async () => {
|
const columnRight = async () => {
|
||||||
const column = await json('__gm.viewportRect()');
|
const column = await json('__gm.viewportRect()');
|
||||||
return column.x + column.w;
|
return column.x + column.w;
|
||||||
@@ -70,7 +68,6 @@ export async function overflow(t) {
|
|||||||
await drag(bars.left.x, bars.left.y, bars.left.x + by, bars.left.y, { steps: 8 });
|
await drag(bars.left.x, bars.left.y, bars.left.x + by, bars.left.y, { steps: 8 });
|
||||||
const after = await json('__gm.panelSplitters()');
|
const after = await json('__gm.panelSplitters()');
|
||||||
await drag(after.right.x, after.right.y, after.right.x - by, after.right.y, { steps: 8 });
|
await drag(after.right.x, after.right.y, after.right.x - by, after.right.y, { steps: 8 });
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
await widenPanels(250);
|
await widenPanels(250);
|
||||||
const narrow = await json('__gm.tabRow()');
|
const narrow = await json('__gm.tabRow()');
|
||||||
@@ -89,7 +86,6 @@ export async function overflow(t) {
|
|||||||
// The overflow button names them, and picking one brings it into the row.
|
// The overflow button names them, and picking one brings it into the row.
|
||||||
const wanted = narrow.hidden[narrow.hidden.length - 1];
|
const wanted = narrow.hidden[narrow.hidden.length - 1];
|
||||||
await clickText('»');
|
await clickText('»');
|
||||||
await sleep(400);
|
|
||||||
const offered = await json('__gm.overlayTexts()');
|
const offered = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'the overflow button offers every tab that had to go',
|
'the overflow button offers every tab that had to go',
|
||||||
@@ -97,7 +93,6 @@ export async function overflow(t) {
|
|||||||
JSON.stringify({ offered, hidden: narrow.hidden }),
|
JSON.stringify({ offered, hidden: narrow.hidden }),
|
||||||
);
|
);
|
||||||
await clickText(TAB_LABELS[wanted]);
|
await clickText(TAB_LABELS[wanted]);
|
||||||
await sleep(400);
|
|
||||||
const picked = await json('__gm.tabRow()');
|
const picked = await json('__gm.tabRow()');
|
||||||
check(
|
check(
|
||||||
`picking ${TAB_LABELS[wanted]} from the overflow menu opens it and puts it in the row`,
|
`picking ${TAB_LABELS[wanted]} from the overflow menu opens it and puts it in the row`,
|
||||||
@@ -105,8 +100,7 @@ export async function overflow(t) {
|
|||||||
JSON.stringify({ tab: await json('__gm.tab()'), shown: picked.shown.map((b) => b.tab) }),
|
JSON.stringify({ tab: await json('__gm.tab()'), shown: picked.shown.map((b) => b.tab) }),
|
||||||
);
|
);
|
||||||
await widenPanels(-250);
|
await widenPanels(-250);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
const back = await json('__gm.tabRow()');
|
const back = await json('__gm.tabRow()');
|
||||||
check(
|
check(
|
||||||
'giving the room back brings tabs into the row again, and none past the edge',
|
'giving the room back brings tabs into the row again, and none past the edge',
|
||||||
@@ -116,8 +110,7 @@ export async function overflow(t) {
|
|||||||
|
|
||||||
// 14c. Every tab, because each one owns a different set of controls.
|
// 14c. Every tab, because each one owns a different set of controls.
|
||||||
for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'list', 'particles']) {
|
for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'list', 'particles']) {
|
||||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
await showTab(tab);
|
||||||
await sleep(400);
|
|
||||||
const spills = await json('__gm.textOverflows()');
|
const spills = await json('__gm.textOverflows()');
|
||||||
check(
|
check(
|
||||||
`no label is drawn past its control in the ${tab} tab`,
|
`no label is drawn past its control in the ${tab} tab`,
|
||||||
@@ -127,8 +120,7 @@ export async function overflow(t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 14d. Shortened, not simply dropped: the value must still be readable and marked as cut.
|
// 14d. Shortened, not simply dropped: the value must still be readable and marked as cut.
|
||||||
await evaluate("__gm.showTab('graph')");
|
await showTab('graph');
|
||||||
await sleep(400);
|
|
||||||
const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement'));
|
const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement'));
|
||||||
check(
|
check(
|
||||||
'a value too wide for its dropdown is cut with an ellipsis',
|
'a value too wide for its dropdown is cut with an ellipsis',
|
||||||
@@ -139,8 +131,7 @@ export async function overflow(t) {
|
|||||||
|
|
||||||
// 14e. An inspector full of dropdowns – the parent row and the enum fields – with a selection
|
// 14e. An inspector full of dropdowns – the parent row and the enum fields – with a selection
|
||||||
// whose asset name is now overlong too.
|
// whose asset name is now overlong too.
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
await evaluate("__gm.state.select({ kind: 'object', objectId: 'otex' })");
|
await evaluate("__gm.state.select({ kind: 'object', objectId: 'otex' })");
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const inspector = await json('__gm.textOverflows()');
|
const inspector = await json('__gm.textOverflows()');
|
||||||
@@ -149,8 +140,7 @@ export async function overflow(t) {
|
|||||||
// 14f. The other direction: a control narrow enough to swallow its whole label draws nothing
|
// 14f. 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.
|
// at all, which the overflow walk cannot see – an empty text spills over no edge.
|
||||||
for (const tab of ['scene', 'graph', 'texture', 'container', 'list', 'particles']) {
|
for (const tab of ['scene', 'graph', 'texture', 'container', 'list', 'particles']) {
|
||||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
await showTab(tab);
|
||||||
await sleep(400);
|
|
||||||
const dropped = await json('__gm.labelsDropped()');
|
const dropped = await json('__gm.labelsDropped()');
|
||||||
check(
|
check(
|
||||||
`no button is left without its label in the ${tab} tab`,
|
`no button is left without its label in the ${tab} tab`,
|
||||||
@@ -160,7 +150,6 @@ export async function overflow(t) {
|
|||||||
}
|
}
|
||||||
// The narrowest buttons in the editor sit on a behaviour's variable rows.
|
// The narrowest buttons in the editor sit on a behaviour's variable rows.
|
||||||
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'mover' })");
|
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'mover' })");
|
||||||
await sleep(400);
|
|
||||||
const onVariables = await json('__gm.labelsDropped()');
|
const onVariables = await json('__gm.labelsDropped()');
|
||||||
check(
|
check(
|
||||||
'the variable rows keep their buttons labelled',
|
'the variable rows keep their buttons labelled',
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export async function aspect(t) {
|
|||||||
|
|
||||||
// 15c. Fit moves the width only, to the sheet's ratio: 100 x 64/48.
|
// 15c. Fit moves the width only, to the sheet's ratio: 100 x 64/48.
|
||||||
await clickText('↔ ▾');
|
await clickText('↔ ▾');
|
||||||
await sleep(400);
|
|
||||||
await clickText('Fit to 64 × 48');
|
await clickText('Fit to 64 × 48');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const fitted = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
|
const fitted = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
|
||||||
@@ -107,7 +106,6 @@ export async function aspect(t) {
|
|||||||
|
|
||||||
const before = await json(spriteProp('width'));
|
const before = await json(spriteProp('width'));
|
||||||
await clickText('↕ ▾');
|
await clickText('↕ ▾');
|
||||||
await sleep(400);
|
|
||||||
await clickText('Fit to 40 × 10');
|
await clickText('Fit to 40 × 10');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const framed = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
|
const framed = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
|
||||||
@@ -123,13 +121,11 @@ export async function aspect(t) {
|
|||||||
await select({ kind: 'object', holderId: 'scene1', objectIds: ['otex'], objectId: 'otex' });
|
await select({ kind: 'object', holderId: 'scene1', objectIds: ['otex'], objectId: 'otex' });
|
||||||
await sleep(600);
|
await sleep(600);
|
||||||
await mutate("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides.height = 90");
|
await mutate("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides.height = 90");
|
||||||
await sleep(400);
|
|
||||||
check('the object row offers the fit as well', (await sizeButton('↔')) === 'menu');
|
check('the object row offers the fit as well', (await sizeButton('↔')) === 'menu');
|
||||||
// The crowded case: an overridden, out-of-proportion row carries ↔ ▾ next to ↑ and ↺.
|
// The crowded case: an overridden, out-of-proportion row carries ↔ ▾ next to ↑ and ↺.
|
||||||
check('and shows it beside the override buttons', (await json('__gm.textOverflows()')).length === 0);
|
check('and shows it beside the override buttons', (await json('__gm.textOverflows()')).length === 0);
|
||||||
await shot('43-aspect-beside-the-override-buttons');
|
await shot('43-aspect-beside-the-override-buttons');
|
||||||
await clickText('↔ ▾');
|
await clickText('↔ ▾');
|
||||||
await sleep(400);
|
|
||||||
await clickText('Fit to 64 × 48');
|
await clickText('Fit to 64 × 48');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const override = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
|
const override = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export async function containerTab(t) {
|
|||||||
object,
|
object,
|
||||||
objects,
|
objects,
|
||||||
storedObjects,
|
storedObjects,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
|
|
||||||
@@ -40,7 +41,6 @@ export async function containerTab(t) {
|
|||||||
|
|
||||||
// 16a. The tab opens on the container the project has, and its header says which one.
|
// 16a. The tab opens on the container the project has, and its header says which one.
|
||||||
await openTab(t, 'container');
|
await openTab(t, 'container');
|
||||||
await sleep(300);
|
|
||||||
const template = await json(`__gm.state.project.assets.find(a => a.id === '${containerId}').objects`);
|
const template = await json(`__gm.state.project.assets.find(a => a.id === '${containerId}').objects`);
|
||||||
const count = template.length;
|
const count = template.length;
|
||||||
check(
|
check(
|
||||||
@@ -161,7 +161,6 @@ export async function containerTab(t) {
|
|||||||
|
|
||||||
// 16f. Delete works on a template object like on any other, as one undo step.
|
// 16f. Delete works on a template object like on any other, as one undo step.
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'Delete removes a template object',
|
'Delete removes a template object',
|
||||||
(await json(`__gm.state.project.assets.find(a => a.id === '${containerId}').objects`)).length === count,
|
(await json(`__gm.state.project.assets.find(a => a.id === '${containerId}').objects`)).length === count,
|
||||||
@@ -173,19 +172,16 @@ export async function containerTab(t) {
|
|||||||
count + 1,
|
count + 1,
|
||||||
);
|
);
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(250);
|
|
||||||
|
|
||||||
// 16g. Fit frames the content. Proven against an object parked far outside the view: without
|
// 16g. Fit frames the content. Proven against an object parked far outside the view: without
|
||||||
// fitting it is nowhere near the canvas.
|
// fitting it is nowhere near the canvas.
|
||||||
await evaluate(
|
await mutate(
|
||||||
`(() => { const t = __gm.state.project.assets.find(a => a.id === '${containerId}').objects.find(o => o.id === '${root.id}'); t.position.x = 3000; __gm.state.preview(); })()`,
|
`const obj = __gm.state.project.assets.find(a => a.id === '${containerId}').objects.find(o => o.id === '${root.id}'); obj.position.x = 3000`,
|
||||||
);
|
);
|
||||||
await sleep(300);
|
|
||||||
const area = await json('__gm.container.rect()');
|
const area = await json('__gm.container.rect()');
|
||||||
const inside = (p) => p.x >= area.x && p.x <= area.x + area.w && p.y >= area.y && p.y <= area.y + area.h;
|
const inside = (p) => p.x >= area.x && p.x <= area.x + area.w && p.y >= area.y && p.y <= area.y + area.h;
|
||||||
const far = await json(`__gm.container.objectScreen(${JSON.stringify(root.id)})`);
|
const far = await json(`__gm.container.objectScreen(${JSON.stringify(root.id)})`);
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(300);
|
|
||||||
const fitted = await json(`__gm.container.objectScreen(${JSON.stringify(root.id)})`);
|
const fitted = await json(`__gm.container.objectScreen(${JSON.stringify(root.id)})`);
|
||||||
check(
|
check(
|
||||||
'Fit brings an object parked far outside back into view',
|
'Fit brings an object parked far outside back into view',
|
||||||
@@ -196,25 +192,18 @@ export async function containerTab(t) {
|
|||||||
`(() => { const t = __gm.state.project.assets.find(a => a.id === '${containerId}').objects.find(o => o.id === '${root.id}'); t.position.x = ${before}; __gm.state.preview(); })()`,
|
`(() => { const t = __gm.state.project.assets.find(a => a.id === '${containerId}').objects.find(o => o.id === '${root.id}'); t.position.x = ${before}; __gm.state.preview(); })()`,
|
||||||
);
|
);
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(300);
|
|
||||||
await shot('32-container-fitted');
|
await shot('32-container-fitted');
|
||||||
|
|
||||||
// 16h. A template object exists only in this tab, so selecting one from elsewhere switches to
|
// 16h. A template object exists only in this tab, so selecting one from elsewhere switches to
|
||||||
// it – the rule a graph node and a texture frame already follow. A scene object does not.
|
// it – the rule a graph node and a texture frame already follow. A scene object does not.
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
await select({ kind: 'object', holderId: containerId, objectId: root.id });
|
||||||
await evaluate(
|
|
||||||
`__gm.state.select({ kind: 'object', holderId: ${JSON.stringify(containerId)}, objectId: ${JSON.stringify(root.id)} })`,
|
|
||||||
);
|
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'selecting a template object switches to the Container tab',
|
'selecting a template object switches to the Container tab',
|
||||||
(await json('__gm.tab()')) === 'container',
|
(await json('__gm.tab()')) === 'container',
|
||||||
);
|
);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
await evaluate("__gm.state.select({ kind: 'object', holderId: 'scene1', objectId: 'otex' })");
|
await evaluate("__gm.state.select({ kind: 'object', holderId: 'scene1', objectId: 'otex' })");
|
||||||
await sleep(300);
|
|
||||||
check('selecting a scene object leaves the Scene tab alone', (await json('__gm.tab()')) === 'scene');
|
check('selecting a scene object leaves the Scene tab alone', (await json('__gm.tab()')) === 'scene');
|
||||||
check(
|
check(
|
||||||
'the hierarchy is back on the scene with it',
|
'the hierarchy is back on the scene with it',
|
||||||
@@ -224,15 +213,13 @@ export async function containerTab(t) {
|
|||||||
|
|
||||||
// 16i. Switching to another container is a plain asset selection – the same thing the header's
|
// 16i. Switching to another container is a plain asset selection – the same thing the header's
|
||||||
// picker does. The hierarchy has to follow it, title and rows both.
|
// picker does. The hierarchy has to follow it, title and rows both.
|
||||||
await evaluate("__gm.showTab('container')");
|
await showTab('container');
|
||||||
await sleep(300);
|
|
||||||
await mutate(`const src = __gm.state.project.assets.find(a => a.id === '${containerId}');
|
await mutate(`const src = __gm.state.project.assets.find(a => a.id === '${containerId}');
|
||||||
const copy = JSON.parse(JSON.stringify(src));
|
const copy = JSON.parse(JSON.stringify(src));
|
||||||
copy.id = 'c2'; copy.name = 'Spare box';
|
copy.id = 'c2'; copy.name = 'Spare box';
|
||||||
copy.objects = copy.objects.slice(0, 1).map((o) => ({ ...o, id: 'c2o1', parentId: '' }));
|
copy.objects = copy.objects.slice(0, 1).map((o) => ({ ...o, id: 'c2o1', parentId: '' }));
|
||||||
__gm.state.project.assets.push(copy)`);
|
__gm.state.project.assets.push(copy)`);
|
||||||
await select({ kind: 'asset', assetId: 'c2' });
|
await select({ kind: 'asset', assetId: 'c2' });
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'picking another container moves the tab and the hierarchy to it',
|
'picking another container moves the tab and the hierarchy to it',
|
||||||
(await json('__gm.container.current()')) === 'c2' &&
|
(await json('__gm.container.current()')) === 'c2' &&
|
||||||
@@ -303,8 +290,7 @@ export async function containerTab(t) {
|
|||||||
// object, and the inner container's content appears under `<instance>~<outer>/<inner>`.
|
// object, and the inner container's content appears under `<instance>~<outer>/<inner>`.
|
||||||
const innerObject = nestedAfter.find((o) => o.assetId === containerId);
|
const innerObject = nestedAfter.find((o) => o.assetId === containerId);
|
||||||
const innerTemplate = await json(`__gm.state.project.assets.find(a => a.id === '${containerId}').objects`);
|
const innerTemplate = await json(`__gm.state.project.assets.find(a => a.id === '${containerId}').objects`);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
const storedBefore = (await storedObjects()).length;
|
const storedBefore = (await storedObjects()).length;
|
||||||
const listedBefore = (await objects()).length;
|
const listedBefore = (await objects()).length;
|
||||||
const spot = await json('__gm.viewportCenter()');
|
const spot = await json('__gm.viewportCenter()');
|
||||||
@@ -331,5 +317,4 @@ export async function containerTab(t) {
|
|||||||
await shot('34-container-nested');
|
await shot('34-container-nested');
|
||||||
await undo(); // the placement
|
await undo(); // the placement
|
||||||
await undo(); // the nesting
|
await undo(); // the nesting
|
||||||
await sleep(200);
|
|
||||||
}
|
}
|
||||||
+17
-11
@@ -43,7 +43,21 @@ const driftGraph = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function editorRun(t) {
|
export async function editorRun(t) {
|
||||||
const { check, sleep, evaluate, json, shot, click, holdKey, mutate, undo, object, api, projectId } = t;
|
const {
|
||||||
|
check,
|
||||||
|
sleep,
|
||||||
|
evaluate,
|
||||||
|
json,
|
||||||
|
shot,
|
||||||
|
click,
|
||||||
|
holdKey,
|
||||||
|
mutate,
|
||||||
|
undo,
|
||||||
|
object,
|
||||||
|
api,
|
||||||
|
projectId,
|
||||||
|
showTab,
|
||||||
|
} = t;
|
||||||
|
|
||||||
// 17a. A box with a drifting behavior below it. Written into the model directly: what is under
|
// 17a. A box with a drifting behavior below it. Written into the model directly: what is under
|
||||||
// test is the switch, not another way of building a graph.
|
// test is the switch, not another way of building a graph.
|
||||||
@@ -62,7 +76,6 @@ export async function editorRun(t) {
|
|||||||
${MAKE_OBJECT}
|
${MAKE_OBJECT}
|
||||||
scene.objects.push(make('er-box', 'crate', '', 400, 'Drift box'), make('er-logic', 'drifter', 'er-box', 0, 'Drifter'));
|
scene.objects.push(make('er-box', 'crate', '', 400, 'Drift box'), make('er-logic', 'drifter', 'er-box', 0, 'Drifter'));
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
const boxX = async () => (await object('er-box')).position.x;
|
const boxX = async () => (await object('er-box')).position.x;
|
||||||
const startX = await boxX();
|
const startX = await boxX();
|
||||||
@@ -93,7 +106,6 @@ export async function editorRun(t) {
|
|||||||
JSON.stringify({ enabledBox, runBox, variables }),
|
JSON.stringify({ enabledBox, runBox, variables }),
|
||||||
);
|
);
|
||||||
await click(runBox.x, runBox.y);
|
await click(runBox.x, runBox.y);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
const logic = await object('er-logic');
|
const logic = await object('er-logic');
|
||||||
check(
|
check(
|
||||||
@@ -144,7 +156,6 @@ export async function editorRun(t) {
|
|||||||
|
|
||||||
// 17e. Taking the switch back stops it again.
|
// 17e. Taking the switch back stops it again.
|
||||||
await click(runBox.x, runBox.y);
|
await click(runBox.x, runBox.y);
|
||||||
await sleep(300);
|
|
||||||
const stoppedAt = await boxX();
|
const stoppedAt = await boxX();
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const stopped = await json('__gm.editorRun()');
|
const stopped = await json('__gm.editorRun()');
|
||||||
@@ -158,7 +169,6 @@ export async function editorRun(t) {
|
|||||||
// undo re-enables it, so it briefly runs again: undo rewinds the switch, never the clock.
|
// undo re-enables it, so it briefly runs again: undo rewinds the switch, never the clock.
|
||||||
await undo();
|
await undo();
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
const afterUndo = await object('er-box');
|
const afterUndo = await object('er-box');
|
||||||
check(
|
check(
|
||||||
'undo takes back the switch, never the movement it caused',
|
'undo takes back the switch, never the movement it caused',
|
||||||
@@ -244,12 +254,10 @@ export async function editorRun(t) {
|
|||||||
`${JSON.stringify(onScene)} x=${sceneBefore} → ${await walkX()}`,
|
`${JSON.stringify(onScene)} x=${sceneBefore} → ${await walkX()}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await evaluate(`__gm.showTab('game')`);
|
await showTab('game');
|
||||||
await sleep(300);
|
|
||||||
// Clicking the view first: a checkbox left focused by an earlier check would still want the keys.
|
// Clicking the view first: a checkbox left focused by an earlier check would still want the keys.
|
||||||
const gameCentre = await json('__gm.gameCenter()');
|
const gameCentre = await json('__gm.gameCenter()');
|
||||||
await click(gameCentre.x, gameCentre.y);
|
await click(gameCentre.x, gameCentre.y);
|
||||||
await sleep(200);
|
|
||||||
const onGame = await json('__gm.editorRun()');
|
const onGame = await json('__gm.editorRun()');
|
||||||
const gameBefore = await walkX();
|
const gameBefore = await walkX();
|
||||||
await holdKey('ArrowRight', 500);
|
await holdKey('ArrowRight', 500);
|
||||||
@@ -268,8 +276,7 @@ export async function editorRun(t) {
|
|||||||
);
|
);
|
||||||
await shot('37-editor-run-keys');
|
await shot('37-editor-run-keys');
|
||||||
|
|
||||||
await evaluate(`__gm.showTab('scene')`);
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
const backOnScene = await walkX();
|
const backOnScene = await walkX();
|
||||||
await holdKey('ArrowRight', 400);
|
await holdKey('ArrowRight', 400);
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
@@ -285,7 +292,6 @@ export async function editorRun(t) {
|
|||||||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('er-'));
|
scene.objects = scene.objects.filter((o) => !o.id.startsWith('er-'));
|
||||||
p.assets = p.assets.filter((a) => !['drifter', 'walker', 'er-level'].includes(a.id));
|
p.assets = p.assets.filter((a) => !['drifter', 'walker', 'er-level'].includes(a.id));
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing of its own behind',
|
'the step leaves nothing of its own behind',
|
||||||
(await json(`__gm.sceneObjects().filter((o) => o.id.startsWith('er-')).length`)) === 0 &&
|
(await json(`__gm.sceneObjects().filter((o) => o.id.startsWith('er-')).length`)) === 0 &&
|
||||||
|
|||||||
+4
-14
@@ -46,6 +46,7 @@ export async function pause(t) {
|
|||||||
select,
|
select,
|
||||||
evaluate,
|
evaluate,
|
||||||
asset,
|
asset,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
const playInfo = () => json('__gm.playInfo()');
|
const playInfo = () => json('__gm.playInfo()');
|
||||||
const boxX = async () => (await entity('p-box'))?.x ?? NaN;
|
const boxX = async () => (await entity('p-box'))?.x ?? NaN;
|
||||||
@@ -78,7 +79,6 @@ export async function pause(t) {
|
|||||||
// 18b. The toolbar button holds it. Both the clock and the box have to stop – a world that keeps
|
// 18b. The toolbar button holds it. Both the clock and the box have to stop – a world that keeps
|
||||||
// ticking while the picture happens to look the same would pass a position check alone.
|
// ticking while the picture happens to look the same would pass a position check alone.
|
||||||
await clickText('❚❚ Pause');
|
await clickText('❚❚ Pause');
|
||||||
await sleep(200);
|
|
||||||
const held = await playInfo();
|
const held = await playInfo();
|
||||||
const heldX = await boxX();
|
const heldX = await boxX();
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
@@ -108,11 +108,9 @@ export async function pause(t) {
|
|||||||
const hitsWhilePaused = await keyHits();
|
const hitsWhilePaused = await keyHits();
|
||||||
await press('ArrowRight');
|
await press('ArrowRight');
|
||||||
await press('ArrowRight');
|
await press('ArrowRight');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 18d. Ctrl+Enter resumes, the same shortcut that started play.
|
// 18d. Ctrl+Enter resumes, the same shortcut that started play.
|
||||||
await press('Enter', { ctrl: true });
|
await press('Enter', { ctrl: true });
|
||||||
await sleep(400);
|
|
||||||
const resumed = await playInfo();
|
const resumed = await playInfo();
|
||||||
const resumedX = await boxX();
|
const resumedX = await boxX();
|
||||||
check(
|
check(
|
||||||
@@ -132,7 +130,6 @@ export async function pause(t) {
|
|||||||
);
|
);
|
||||||
// Without this the check above would also pass on a graph that never logs anything at all.
|
// Without this the check above would also pass on a graph that never logs anything at all.
|
||||||
await press('ArrowRight');
|
await press('ArrowRight');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the same key does fire once the game is running again',
|
'the same key does fire once the game is running again',
|
||||||
(await keyHits()) === hitsBefore + 1,
|
(await keyHits()) === hitsBefore + 1,
|
||||||
@@ -142,7 +139,6 @@ export async function pause(t) {
|
|||||||
// 18e. A hold must never outlive its play: Esc stops from a paused game, and the next run starts
|
// 18e. A hold must never outlive its play: Esc stops from a paused game, and the next run starts
|
||||||
// moving. A pause left standing would leave the editor unable to run anything.
|
// moving. A pause left standing would leave the editor unable to run anything.
|
||||||
await press('Enter', { ctrl: true });
|
await press('Enter', { ctrl: true });
|
||||||
await sleep(200);
|
|
||||||
check('Ctrl+Enter holds it again', (await playInfo()).paused === true);
|
check('Ctrl+Enter holds it again', (await playInfo()).paused === true);
|
||||||
await stop();
|
await stop();
|
||||||
const afterStop = await playInfo();
|
const afterStop = await playInfo();
|
||||||
@@ -162,7 +158,9 @@ export async function pause(t) {
|
|||||||
// 18f. The graph stays editable while the game runs, with the mouse as in edit mode: a click
|
// 18f. The graph stays editable while the game runs, with the mouse as in edit mode: a click
|
||||||
// selects the Move node, Delete removes it, and the running world stops drifting the box.
|
// selects the Move node, Delete removes it, and the running world stops drifting the box.
|
||||||
await select({ kind: 'asset', assetId: 'pauser' });
|
await select({ kind: 'asset', assetId: 'pauser' });
|
||||||
await evaluate(`__gm.showTab('graph')`);
|
await showTab('graph');
|
||||||
|
// The live values are not drawn, they are carried: the running graph has to execute once while
|
||||||
|
// this tab is open before a node output holds anything. That is game time, not frames.
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
const outputs = (id) => json(`__gm.graph.nodeOutputs(${JSON.stringify(id)})`);
|
const outputs = (id) => json(`__gm.graph.nodeOutputs(${JSON.stringify(id)})`);
|
||||||
const live = {
|
const live = {
|
||||||
@@ -193,7 +191,6 @@ export async function pause(t) {
|
|||||||
);
|
);
|
||||||
const beforeKey = await beadsOn('p-e4');
|
const beforeKey = await beadsOn('p-e4');
|
||||||
await press('ArrowRight');
|
await press('ArrowRight');
|
||||||
await sleep(80);
|
|
||||||
const onKey = await beadsOn('p-e4');
|
const onKey = await beadsOn('p-e4');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const afterKey = await beadsOn('p-e4');
|
const afterKey = await beadsOn('p-e4');
|
||||||
@@ -204,7 +201,6 @@ export async function pause(t) {
|
|||||||
);
|
);
|
||||||
const movePos = await json(`__gm.graph.nodeAt('p-move')`);
|
const movePos = await json(`__gm.graph.nodeAt('p-move')`);
|
||||||
await click(movePos.x, movePos.y);
|
await click(movePos.x, movePos.y);
|
||||||
await sleep(100);
|
|
||||||
check(
|
check(
|
||||||
'a click selects a node while playing',
|
'a click selects a node while playing',
|
||||||
JSON.stringify(await json('__gm.graph.selected()')) === '["p-move"]',
|
JSON.stringify(await json('__gm.graph.selected()')) === '["p-move"]',
|
||||||
@@ -212,17 +208,14 @@ export async function pause(t) {
|
|||||||
);
|
);
|
||||||
const area = await json('__gm.graph.canvasRect()');
|
const area = await json('__gm.graph.canvasRect()');
|
||||||
await click(area.x + area.w - 15, area.y + area.h - 15, 'right');
|
await click(area.x + area.w - 15, area.y + area.h - 15, 'right');
|
||||||
await sleep(100);
|
|
||||||
check('right-clicking the canvas opens the node palette while playing', await has('Search nodes…'));
|
check('right-clicking the canvas opens the node palette while playing', await has('Search nodes…'));
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(100);
|
|
||||||
check(
|
check(
|
||||||
'Escape closes the palette and leaves the game running',
|
'Escape closes the palette and leaves the game running',
|
||||||
!(await has('Search nodes…')) && (await playInfo()).playing === true,
|
!(await has('Search nodes…')) && (await playInfo()).playing === true,
|
||||||
);
|
);
|
||||||
await click(movePos.x, movePos.y);
|
await click(movePos.x, movePos.y);
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(150);
|
|
||||||
const stoppedAt = await boxX();
|
const stoppedAt = await boxX();
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
check(
|
check(
|
||||||
@@ -232,7 +225,6 @@ export async function pause(t) {
|
|||||||
`x ${stoppedAt} → ${await boxX()}`,
|
`x ${stoppedAt} → ${await boxX()}`,
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'stopping takes the values off the nodes',
|
'stopping takes the values off the nodes',
|
||||||
JSON.stringify(await outputs('p-owner')) === '["object"]',
|
JSON.stringify(await outputs('p-owner')) === '["object"]',
|
||||||
@@ -267,7 +259,6 @@ export async function pause(t) {
|
|||||||
await sleep(400);
|
await sleep(400);
|
||||||
const runningClock = await clock();
|
const runningClock = await clock();
|
||||||
await clickText('❚❚ Pause');
|
await clickText('❚❚ Pause');
|
||||||
await sleep(150);
|
|
||||||
const heldClock = await clock();
|
const heldClock = await clock();
|
||||||
await sleep(800);
|
await sleep(800);
|
||||||
const stillHeldClock = await clock();
|
const stillHeldClock = await clock();
|
||||||
@@ -283,7 +274,6 @@ export async function pause(t) {
|
|||||||
// Twice the scale has to move the clock and the world by the same factor: the delta a behavior
|
// Twice the scale has to move the clock and the world by the same factor: the delta a behavior
|
||||||
// reads is the step the clock took, not a second measurement of the wall clock.
|
// reads is the step the clock took, not a second measurement of the wall clock.
|
||||||
await press('Enter', { ctrl: true });
|
await press('Enter', { ctrl: true });
|
||||||
await sleep(150);
|
|
||||||
const slowFrom = { t: await clock(), x: await boxX() };
|
const slowFrom = { t: await clock(), x: await boxX() };
|
||||||
await sleep(600);
|
await sleep(600);
|
||||||
const slowTo = { t: await clock(), x: await boxX() };
|
const slowTo = { t: await clock(), x: await boxX() };
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ export async function nestedBehavior(t) {
|
|||||||
${MAKE_OBJECT}
|
${MAKE_OBJECT}
|
||||||
scene.objects.push(make('nb-box', 'crate', '', 700, 'Nested box'), make('nb-logic', 'nb-caller', 'nb-box', 700, 'Caller'));
|
scene.objects.push(make('nb-box', 'crate', '', 700, 'Nested box'), make('nb-logic', 'nb-caller', 'nb-box', 700, 'Caller'));
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 19b. The call really runs, and the value really comes back: the box moves at dx × scale, and
|
// 19b. The call really runs, and the value really comes back: the box moves at dx × scale, and
|
||||||
// dx is nothing the caller holds – it is what the nested graph returned. Without the result
|
// dx is nothing the caller holds – it is what the nested graph returned. Without the result
|
||||||
@@ -95,7 +94,6 @@ export async function nestedBehavior(t) {
|
|||||||
const boxX = async () => (await object('nb-box')).position.x;
|
const boxX = async () => (await object('nb-box')).position.x;
|
||||||
const idleX = await boxX();
|
const idleX = await boxX();
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(300);
|
|
||||||
const before = (await entity('nb-box')).x;
|
const before = (await entity('nb-box')).x;
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
const after = (await entity('nb-box')).x;
|
const after = (await entity('nb-box')).x;
|
||||||
@@ -106,7 +104,6 @@ export async function nestedBehavior(t) {
|
|||||||
`moved ${speed.toFixed(1)} px in ~1 s – 2 × ${SCALE} means the returned value arrived, 0 would mean nothing came back`,
|
`moved ${speed.toFixed(1)} px in ~1 s – 2 × ${SCALE} means the returned value arrived, 0 would mean nothing came back`,
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the stored object is untouched by the run, as in any play session',
|
'the stored object is untouched by the run, as in any play session',
|
||||||
(await boxX()) === idleX,
|
(await boxX()) === idleX,
|
||||||
@@ -115,10 +112,7 @@ export async function nestedBehavior(t) {
|
|||||||
|
|
||||||
// 19c. The node in the graph carries the *other* behavior's ports and name – neither is part of
|
// 19c. The node in the graph carries the *other* behavior's ports and name – neither is part of
|
||||||
// its own type.
|
// its own type.
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] });
|
||||||
`__gm.state.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] })`,
|
|
||||||
);
|
|
||||||
await sleep(300);
|
|
||||||
const inPort = await portAt('nb-call', 'in1', false);
|
const inPort = await portAt('nb-call', 'in1', false);
|
||||||
const outPort = await portAt('nb-call', 'out1', true);
|
const outPort = await portAt('nb-call', 'out1', true);
|
||||||
check(
|
check(
|
||||||
@@ -137,27 +131,19 @@ export async function nestedBehavior(t) {
|
|||||||
// 19d. Declaring a port on one behavior reaches the node in the other one, live. This is the
|
// 19d. Declaring a port on one behavior reaches the node in the other one, live. This is the
|
||||||
// check that a port list derived from another graph is really derived and not copied.
|
// check that a port list derived from another graph is really derived and not copied.
|
||||||
check('the calling node has no second input yet', !(await portAt('nb-call', 'in2', false)));
|
check('the calling node has no second input yet', !(await portAt('nb-call', 'in2', false)));
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: 'nb-double', nodeId: 'nb-start', nodeIds: ['nb-start'] });
|
||||||
`__gm.state.select({ kind: 'node', assetId: 'nb-double', nodeId: 'nb-start', nodeIds: ['nb-start'] })`,
|
|
||||||
);
|
|
||||||
await sleep(300);
|
|
||||||
const addButton = await locate('+ Input');
|
const addButton = await locate('+ Input');
|
||||||
check('the start node offers its inputs in the inspector', Boolean(addButton), JSON.stringify(addButton));
|
check('the start node offers its inputs in the inspector', Boolean(addButton), JSON.stringify(addButton));
|
||||||
await shot('38-behavior-ports');
|
await shot('38-behavior-ports');
|
||||||
await click(addButton.x, addButton.y);
|
await click(addButton.x, addButton.y);
|
||||||
await sleep(200);
|
|
||||||
await clickText('Number');
|
await clickText('Number');
|
||||||
await sleep(300);
|
|
||||||
const declared = await startPorts();
|
const declared = await startPorts();
|
||||||
check(
|
check(
|
||||||
'adding an input declares it on the start node',
|
'adding an input declares it on the start node',
|
||||||
declared.length === 2 && declared[1].key === 'in2' && declared[1].kind === 'number',
|
declared.length === 2 && declared[1].key === 'in2' && declared[1].kind === 'number',
|
||||||
JSON.stringify(declared),
|
JSON.stringify(declared),
|
||||||
);
|
);
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] });
|
||||||
`__gm.state.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] })`,
|
|
||||||
);
|
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'and the node in the other graph grew that input without being touched',
|
'and the node in the other graph grew that input without being touched',
|
||||||
Boolean(await portAt('nb-call', 'in2', false)),
|
Boolean(await portAt('nb-call', 'in2', false)),
|
||||||
@@ -165,7 +151,6 @@ export async function nestedBehavior(t) {
|
|||||||
|
|
||||||
// 19e. Undo takes the port back, and with it the port on the calling node.
|
// 19e. Undo takes the port back, and with it the port on the calling node.
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
const afterUndo = await startPorts();
|
const afterUndo = await startPorts();
|
||||||
check(
|
check(
|
||||||
'undo removes it in both graphs',
|
'undo removes it in both graphs',
|
||||||
@@ -180,14 +165,11 @@ export async function nestedBehavior(t) {
|
|||||||
const idsBefore = await nodeIds();
|
const idsBefore = await nodeIds();
|
||||||
const bare = await json('__gm.graph.emptySpot()');
|
const bare = await json('__gm.graph.emptySpot()');
|
||||||
await click(bare.x, bare.y, 'right');
|
await click(bare.x, bare.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
check('node palette opens', Boolean(await locate('Search nodes…')));
|
check('node palette opens', Boolean(await locate('Search nodes…')));
|
||||||
await typeText('Double');
|
await typeText('Double');
|
||||||
await sleep(200);
|
|
||||||
// Enter takes the first match. NOT a click on the label: "Double" also names the asset in the
|
// Enter takes the first match. NOT a click on the label: "Double" also names the asset in the
|
||||||
// list on the left, and that is what a click by text would find first.
|
// list on the left, and that is what a click by text would find first.
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(300);
|
|
||||||
const added = (await asset('nb-caller')).graph.nodes.find((n) => !idsBefore.includes(n.id));
|
const added = (await asset('nb-caller')).graph.nodes.find((n) => !idsBefore.includes(n.id));
|
||||||
check(
|
check(
|
||||||
'picking a behavior in the palette drops in a node already set to it',
|
'picking a behavior in the palette drops in a node already set to it',
|
||||||
@@ -197,7 +179,6 @@ export async function nestedBehavior(t) {
|
|||||||
JSON.stringify(added && { type: added.type, params: added.params }),
|
JSON.stringify(added && { type: added.type, params: added.params }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 19g. Dragging a behavior out of the asset panel drops a call into the graph, where the
|
// 19g. Dragging a behavior out of the asset panel drops a call into the graph, where the
|
||||||
// pointer let go of it.
|
// pointer let go of it.
|
||||||
@@ -213,7 +194,6 @@ export async function nestedBehavior(t) {
|
|||||||
});
|
});
|
||||||
const dropAt = spot(0.25, 0.75);
|
const dropAt = spot(0.25, 0.75);
|
||||||
await t.dragAsset('nb-double', dropAt.x, dropAt.y);
|
await t.dragAsset('nb-double', dropAt.x, dropAt.y);
|
||||||
await sleep(300);
|
|
||||||
const dropped = await newNodeIn(beforeDrag);
|
const dropped = await newNodeIn(beforeDrag);
|
||||||
check(
|
check(
|
||||||
'a behavior dragged from the asset panel becomes a call node',
|
'a behavior dragged from the asset panel becomes a call node',
|
||||||
@@ -231,7 +211,6 @@ export async function nestedBehavior(t) {
|
|||||||
const beforeEmpty = await nodeIds();
|
const beforeEmpty = await nodeIds();
|
||||||
const emptyRow = await json(`__gm.assetRow('nb-empty')`);
|
const emptyRow = await json(`__gm.assetRow('nb-empty')`);
|
||||||
await t.dragAsset('nb-empty', spot(0.6, 0.75).x, spot(0.6, 0.75).y);
|
await t.dragAsset('nb-empty', spot(0.6, 0.75).x, spot(0.6, 0.75).y);
|
||||||
await sleep(300);
|
|
||||||
const emptyCall = await newNodeIn(beforeEmpty);
|
const emptyCall = await newNodeIn(beforeEmpty);
|
||||||
check(
|
check(
|
||||||
'a behavior without a start node can be added just the same',
|
'a behavior without a start node can be added just the same',
|
||||||
@@ -255,7 +234,6 @@ export async function nestedBehavior(t) {
|
|||||||
const beforeSelf = await nodeIds();
|
const beforeSelf = await nodeIds();
|
||||||
const selfRow = await json(`__gm.assetRow('nb-caller')`);
|
const selfRow = await json(`__gm.assetRow('nb-caller')`);
|
||||||
await t.dragAsset('nb-caller', spot(0.45, 0.5).x, spot(0.45, 0.5).y);
|
await t.dragAsset('nb-caller', spot(0.45, 0.5).x, spot(0.45, 0.5).y);
|
||||||
await sleep(300);
|
|
||||||
const afterSelf = await nodeIds();
|
const afterSelf = await nodeIds();
|
||||||
const { notice } = await t.state();
|
const { notice } = await t.state();
|
||||||
// `dragging` tells a refusal apart from a drag that never arrived – both leave the graph alone.
|
// `dragging` tells a refusal apart from a drag that never arrived – both leave the graph alone.
|
||||||
@@ -277,7 +255,6 @@ export async function nestedBehavior(t) {
|
|||||||
check('the graph shown is still the caller', (await evaluate('__gm.graph.current()')) === 'nb-caller');
|
check('the graph shown is still the caller', (await evaluate('__gm.graph.current()')) === 'nb-caller');
|
||||||
check('no breadcrumb on a graph opened from the outside', (await json('__gm.graph.crumbs()')).length === 0);
|
check('no breadcrumb on a graph opened from the outside', (await json('__gm.graph.crumbs()')).length === 0);
|
||||||
await t.doubleClick(callNode.x, callNode.y);
|
await t.doubleClick(callNode.x, callNode.y);
|
||||||
await sleep(400);
|
|
||||||
const opened = await evaluate('__gm.graph.current()');
|
const opened = await evaluate('__gm.graph.current()');
|
||||||
check(
|
check(
|
||||||
'double-clicking a call node opens the behavior it runs',
|
'double-clicking a call node opens the behavior it runs',
|
||||||
@@ -307,7 +284,6 @@ export async function nestedBehavior(t) {
|
|||||||
await trail(),
|
await trail(),
|
||||||
);
|
);
|
||||||
await t.doubleClick(inner.x, inner.y);
|
await t.doubleClick(inner.x, inner.y);
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'each call opened further down adds a crumb',
|
'each call opened further down adds a crumb',
|
||||||
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await trail()) === 'Caller › Double › Empty*',
|
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await trail()) === 'Caller › Double › Empty*',
|
||||||
@@ -330,7 +306,6 @@ export async function nestedBehavior(t) {
|
|||||||
const clickCrumb = async (name) => {
|
const clickCrumb = async (name) => {
|
||||||
const crumb = (await crumbs()).find((c) => c.name === name);
|
const crumb = (await crumbs()).find((c) => c.name === name);
|
||||||
await click(crumb.x, crumb.y);
|
await click(crumb.x, crumb.y);
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
await clickCrumb('Double');
|
await clickCrumb('Double');
|
||||||
await shot('39c-breadcrumb-back');
|
await shot('39c-breadcrumb-back');
|
||||||
@@ -350,12 +325,10 @@ export async function nestedBehavior(t) {
|
|||||||
const enterDouble = async () => {
|
const enterDouble = async () => {
|
||||||
const call = await json(`__gm.graph.nodeAt('nb-call')`);
|
const call = await json(`__gm.graph.nodeAt('nb-call')`);
|
||||||
await t.doubleClick(call.x, call.y);
|
await t.doubleClick(call.x, call.y);
|
||||||
await sleep(400);
|
|
||||||
};
|
};
|
||||||
await enterDouble();
|
await enterDouble();
|
||||||
const emptyAgain = await json(`__gm.assetRow('nb-empty')`);
|
const emptyAgain = await json(`__gm.assetRow('nb-empty')`);
|
||||||
await click(emptyAgain.x, emptyAgain.y);
|
await click(emptyAgain.x, emptyAgain.y);
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'choosing another behavior in the asset panel ends the trail',
|
'choosing another behavior in the asset panel ends the trail',
|
||||||
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
|
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
|
||||||
@@ -366,10 +339,8 @@ export async function nestedBehavior(t) {
|
|||||||
await enterDouble();
|
await enterDouble();
|
||||||
const picker = await json('__gm.graph.pickerAt()');
|
const picker = await json('__gm.graph.pickerAt()');
|
||||||
await click(picker.x, picker.y);
|
await click(picker.x, picker.y);
|
||||||
await sleep(150);
|
|
||||||
const emptyItem = await locate('Empty', true);
|
const emptyItem = await locate('Empty', true);
|
||||||
await click(emptyItem.x, emptyItem.y);
|
await click(emptyItem.x, emptyItem.y);
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'choosing another behavior in the picker ends the trail',
|
'choosing another behavior in the picker ends the trail',
|
||||||
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
|
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
|
||||||
@@ -385,7 +356,6 @@ export async function nestedBehavior(t) {
|
|||||||
p.assets = p.assets.filter((a) => !mine.includes(a.id));
|
p.assets = p.assets.filter((a) => !mine.includes(a.id));
|
||||||
scene.objects = scene.objects.filter((o) => o.id !== 'nb-box' && o.id !== 'nb-logic');
|
scene.objects = scene.objects.filter((o) => o.id !== 'nb-box' && o.id !== 'nb-logic');
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
const left = await json(`{
|
const left = await json(`{
|
||||||
objects: __gm.sceneObjects().filter((o) => o.id.startsWith('nb-')).length,
|
objects: __gm.sceneObjects().filter((o) => o.id.startsWith('nb-')).length,
|
||||||
assets: __gm.state.project.assets.filter((a) => a.id.startsWith('nb-')).length,
|
assets: __gm.state.project.assets.filter((a) => a.id.startsWith('nb-')).length,
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export async function keyRepeat(t) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 20b. A hold shorter than the delay is still just a press. Without this the check below would
|
// 20b. A hold shorter than the delay is still just a press. Without this the check below would
|
||||||
// also pass on a node that simply fires every frame the key is down.
|
// also pass on a node that simply fires every frame the key is down.
|
||||||
@@ -75,11 +74,9 @@ export async function keyRepeat(t) {
|
|||||||
`plain +${afterLong.plain - afterShort.plain}, repeating +${gained} over 800 ms (press + ~${Math.round((800 - DELAY) / INTERVAL)} repeats expected)`,
|
`plain +${afterLong.plain - afterShort.plain}, repeating +${gained} over 800 ms (press + ~${Math.round((800 - DELAY) / INTERVAL)} repeats expected)`,
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 20d. Both settings are on the node itself, so a graph can be tuned where it is built.
|
// 20d. Both settings are on the node itself, so a graph can be tuned where it is built.
|
||||||
await select({ kind: 'node', assetId: 'repeater', nodeId: 'r-repeat' });
|
await select({ kind: 'node', assetId: 'repeater', nodeId: 'r-repeat' });
|
||||||
await sleep(300);
|
|
||||||
const afterField = await json(`__gm.inspectorField('Repeat after (ms)')`);
|
const afterField = await json(`__gm.inspectorField('Repeat after (ms)')`);
|
||||||
const intervalField = await json(`__gm.inspectorField('Repeat interval (ms)')`);
|
const intervalField = await json(`__gm.inspectorField('Repeat interval (ms)')`);
|
||||||
check(
|
check(
|
||||||
@@ -105,7 +102,6 @@ export async function keyRepeat(t) {
|
|||||||
await click(afterField.x, afterField.y);
|
await click(afterField.x, afterField.y);
|
||||||
await typeText('750');
|
await typeText('750');
|
||||||
await click(intervalField.x, intervalField.y);
|
await click(intervalField.x, intervalField.y);
|
||||||
await sleep(200);
|
|
||||||
const params = await json(
|
const params = await json(
|
||||||
`__gm.state.project.assets.find((a) => a.id === 'repeater').graph.nodes.find((n) => n.id === 'r-repeat').params`,
|
`__gm.state.project.assets.find((a) => a.id === 'repeater').graph.nodes.find((n) => n.id === 'r-repeat').params`,
|
||||||
);
|
);
|
||||||
@@ -118,7 +114,6 @@ export async function keyRepeat(t) {
|
|||||||
// 20e. The typed value is what the next run obeys: 750 ms is longer than this hold, so the
|
// 20e. The typed value is what the next run obeys: 750 ms is longer than this hold, so the
|
||||||
// repeating node falls silent although nothing else about the graph changed.
|
// repeating node falls silent although nothing else about the graph changed.
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(300);
|
|
||||||
const beforeTuned = await hits();
|
const beforeTuned = await hits();
|
||||||
await holdKey('ArrowRight', 500);
|
await holdKey('ArrowRight', 500);
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
@@ -129,7 +124,6 @@ export async function keyRepeat(t) {
|
|||||||
`repeating ${beforeTuned.repeating} → ${afterTuned.repeating} over 500 ms with the delay raised to 750 ms`,
|
`repeating ${beforeTuned.repeating} → ${afterTuned.repeating} over 500 ms with the delay raised to 750 ms`,
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
const left = await rig.remove();
|
const left = await rig.remove();
|
||||||
check(
|
check(
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export async function listTab(t) {
|
|||||||
});
|
});
|
||||||
__gm.state.select({ kind: 'asset', assetId: 'listy' });
|
__gm.state.select({ kind: 'asset', assetId: 'listy' });
|
||||||
`);
|
`);
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
// 21a. The inspector shows the count, not a comma-separated line.
|
// 21a. The inspector shows the count, not a comma-separated line.
|
||||||
const button = await find('3 items');
|
const button = await find('3 items');
|
||||||
@@ -40,7 +39,6 @@ export async function listTab(t) {
|
|||||||
await shot('01-count-button');
|
await shot('01-count-button');
|
||||||
|
|
||||||
await click(button.x, button.y);
|
await click(button.x, button.y);
|
||||||
await sleep(200);
|
|
||||||
check('clicking it switches to the List tab', (await json('__gm.tab()')) === 'list');
|
check('clicking it switches to the List tab', (await json('__gm.tab()')) === 'list');
|
||||||
let info = await json('__gm.list.info()');
|
let info = await json('__gm.list.info()');
|
||||||
check('which shows the list it was opened from', info.title === 'Listy · nums', JSON.stringify(info));
|
check('which shows the list it was opened from', info.title === 'Listy · nums', JSON.stringify(info));
|
||||||
@@ -53,7 +51,6 @@ export async function listTab(t) {
|
|||||||
await click(second.x, second.y);
|
await click(second.x, second.y);
|
||||||
await typeText('42');
|
await typeText('42');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
info = await json('__gm.list.info()');
|
info = await json('__gm.list.info()');
|
||||||
check('typing in a row writes that item', JSON.stringify(info.items) === '["30","42","44"]', info.items[1]);
|
check('typing in a row writes that item', JSON.stringify(info.items) === '["30","42","44"]', info.items[1]);
|
||||||
await undo();
|
await undo();
|
||||||
@@ -67,7 +64,6 @@ export async function listTab(t) {
|
|||||||
a.props.nums = Array.from({ length: 60 }, (_, i) => i);
|
a.props.nums = Array.from({ length: 60 }, (_, i) => i);
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(250);
|
|
||||||
const longRows = [await json('__gm.list.fieldAt(0)'), await json('__gm.list.fieldAt(1)')];
|
const longRows = [await json('__gm.list.fieldAt(0)'), await json('__gm.list.fieldAt(1)')];
|
||||||
check('a long list stacks its rows', longRows[0].y !== longRows[1].y, JSON.stringify(longRows));
|
check('a long list stacks its rows', longRows[0].y !== longRows[1].y, JSON.stringify(longRows));
|
||||||
await mutate(`
|
await mutate(`
|
||||||
@@ -75,7 +71,6 @@ export async function listTab(t) {
|
|||||||
a.props.nums = [30, 11, 44];
|
a.props.nums = [30, 11, 44];
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(250);
|
|
||||||
const shortRows = [await json('__gm.list.fieldAt(0)'), await json('__gm.list.fieldAt(1)')];
|
const shortRows = [await json('__gm.list.fieldAt(0)'), await json('__gm.list.fieldAt(1)')];
|
||||||
check(
|
check(
|
||||||
'and a short list after it still stacks its own',
|
'and a short list after it still stacks its own',
|
||||||
@@ -85,7 +80,6 @@ export async function listTab(t) {
|
|||||||
|
|
||||||
// 21d. `+ Item` asks for a kind, since the list does not hold one for it.
|
// 21d. `+ Item` asks for a kind, since the list does not hold one for it.
|
||||||
await clickText('+ Item');
|
await clickText('+ Item');
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'a list of its own offers every kind, the struct included',
|
'a list of its own offers every kind, the struct included',
|
||||||
(await json('__gm.list.info()')).offers.join(',') ===
|
(await json('__gm.list.info()')).offers.join(',') ===
|
||||||
@@ -109,23 +103,19 @@ export async function listTab(t) {
|
|||||||
|
|
||||||
// 21e. Back on the scene tab the button carries the current count.
|
// 21e. Back on the scene tab the button carries the current count.
|
||||||
await mutate('__gm.state.openList(null)');
|
await mutate('__gm.state.openList(null)');
|
||||||
await sleep(200);
|
|
||||||
check('closing the list leaves the tab', (await json('__gm.tab()')) === 'scene');
|
check('closing the list leaves the tab', (await json('__gm.tab()')) === 'scene');
|
||||||
await mutate(`
|
await mutate(`
|
||||||
__gm.state.project.assets.find((a) => a.id === 'listy').props.nums = [1, 2];
|
__gm.state.project.assets.find((a) => a.id === 'listy').props.nums = [1, 2];
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
check('and the count follows the value', (await find('2 items')) !== null);
|
check('and the count follows the value', (await find('2 items')) !== null);
|
||||||
|
|
||||||
// 21f. A list declares nothing about its items, so the kind sits on the row: the mark in front of
|
// 21f. A list declares nothing about its items, so the kind sits on the row: the mark in front of
|
||||||
// it is the picker, and picking changes that one item and no other.
|
// it is the picker, and picking changes that one item and no other.
|
||||||
const numsOf = () => json(`__gm.state.project.assets.find((a) => a.id === 'listy')`);
|
const numsOf = () => json(`__gm.state.project.assets.find((a) => a.id === 'listy')`);
|
||||||
await mutate("__gm.state.openList({ kind: 'asset', assetId: 'listy', key: 'nums' })");
|
await mutate("__gm.state.openList({ kind: 'asset', assetId: 'listy', key: 'nums' })");
|
||||||
await sleep(200);
|
|
||||||
let mark = await json('__gm.list.markAt(0)');
|
let mark = await json('__gm.list.markAt(0)');
|
||||||
await click(mark.x, mark.y);
|
await click(mark.x, mark.y);
|
||||||
await sleep(150);
|
|
||||||
await pickKind('Text');
|
await pickKind('Text');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
let listy = await numsOf();
|
let listy = await numsOf();
|
||||||
@@ -141,7 +131,6 @@ export async function listTab(t) {
|
|||||||
);
|
);
|
||||||
check('the variable itself says nothing about items', listy.variables[0].items === undefined);
|
check('the variable itself says nothing about items', listy.variables[0].items === undefined);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(250);
|
|
||||||
listy = await numsOf();
|
listy = await numsOf();
|
||||||
check(
|
check(
|
||||||
'undo puts the item back as a number',
|
'undo puts the item back as a number',
|
||||||
@@ -152,7 +141,6 @@ export async function listTab(t) {
|
|||||||
// 21g. A colour is the one kind a stored number cannot say for itself, so it is tagged.
|
// 21g. A colour is the one kind a stored number cannot say for itself, so it is tagged.
|
||||||
mark = await json('__gm.list.markAt(1)');
|
mark = await json('__gm.list.markAt(1)');
|
||||||
await click(mark.x, mark.y);
|
await click(mark.x, mark.y);
|
||||||
await sleep(150);
|
|
||||||
await pickKind('Color');
|
await pickKind('Color');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
listy = await numsOf();
|
listy = await numsOf();
|
||||||
@@ -168,7 +156,6 @@ export async function listTab(t) {
|
|||||||
);
|
);
|
||||||
await shot('03b-kinds');
|
await shot('03b-kinds');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 21h. The second tagged kind: a picture is an id, which stored on its own would be a plain text.
|
// 21h. The second tagged kind: a picture is an id, which stored on its own would be a plain text.
|
||||||
// The row carries the picker the `texture` property has, frames of a sheet included.
|
// The row carries the picker the `texture` property has, frames of a sheet included.
|
||||||
@@ -186,10 +173,8 @@ export async function listTab(t) {
|
|||||||
});
|
});
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
mark = await json('__gm.list.markAt(0)');
|
mark = await json('__gm.list.markAt(0)');
|
||||||
await click(mark.x, mark.y);
|
await click(mark.x, mark.y);
|
||||||
await sleep(150);
|
|
||||||
await pickKind('Texture');
|
await pickKind('Texture');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
// Switching kind carries the value along, here into an id that resolves to nothing – the same as
|
// Switching kind carries the value along, here into an id that resolves to nothing – the same as
|
||||||
@@ -201,7 +186,6 @@ export async function listTab(t) {
|
|||||||
);
|
);
|
||||||
const pic = await json('__gm.list.fieldAt(0)');
|
const pic = await json('__gm.list.fieldAt(0)');
|
||||||
await click(pic.x, pic.y);
|
await click(pic.x, pic.y);
|
||||||
await sleep(250);
|
|
||||||
check('the row carries the picture picker, sheet frames and all', (await find('Sheet / Frame 1')) !== null);
|
check('the row carries the picture picker, sheet frames and all', (await find('Sheet / Frame 1')) !== null);
|
||||||
await pickKind('Sheet / Frame 1');
|
await pickKind('Sheet / Frame 1');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
@@ -218,7 +202,6 @@ export async function listTab(t) {
|
|||||||
await shot('03c-texture');
|
await shot('03c-texture');
|
||||||
await undo();
|
await undo();
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'undo puts the plain number back',
|
'undo puts the plain number back',
|
||||||
JSON.stringify((await numsOf()).props.nums) === '[1,2]',
|
JSON.stringify((await numsOf()).props.nums) === '[1,2]',
|
||||||
@@ -230,10 +213,8 @@ export async function listTab(t) {
|
|||||||
delete __gm.__pictures;
|
delete __gm.__pictures;
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
await mutate('__gm.state.openList(null)');
|
await mutate('__gm.state.openList(null)');
|
||||||
await sleep(200);
|
|
||||||
// The mark of a list is plain now, and still may not push the value off the inspector's grid.
|
// The mark of a list is plain now, and still may not push the value off the inspector's grid.
|
||||||
const lefts = await json(
|
const lefts = await json(
|
||||||
`[__gm.inspectorField('Name')?.left ?? null, __gm.inspectorField('nums')?.left ?? null]`,
|
`[__gm.inspectorField('Name')?.left ?? null, __gm.inspectorField('nums')?.left ?? null]`,
|
||||||
@@ -246,7 +227,6 @@ export async function listTab(t) {
|
|||||||
|
|
||||||
// 21i. A list whose target is gone empties the tab rather than showing a stale one.
|
// 21i. A list whose target is gone empties the tab rather than showing a stale one.
|
||||||
await mutate("__gm.state.openList({ kind: 'asset', assetId: 'listy', key: 'nums' })");
|
await mutate("__gm.state.openList({ kind: 'asset', assetId: 'listy', key: 'nums' })");
|
||||||
await sleep(200);
|
|
||||||
check('a list can be opened from outside the inspector too', (await json('__gm.tab()')) === 'list');
|
check('a list can be opened from outside the inspector too', (await json('__gm.tab()')) === 'list');
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const p = __gm.state.project;
|
const p = __gm.state.project;
|
||||||
@@ -254,7 +234,6 @@ export async function listTab(t) {
|
|||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
info = await json('__gm.list.info()');
|
info = await json('__gm.list.info()');
|
||||||
check('a deleted target empties the tab', info.title === '' && info.items.length === 0, info.title);
|
check('a deleted target empties the tab', info.title === '' && info.items.length === 0, info.title);
|
||||||
await shot('04-empty');
|
await shot('04-empty');
|
||||||
@@ -274,7 +253,6 @@ export async function listTab(t) {
|
|||||||
__gm.state.select({ kind: 'asset', assetId: 'celly' });
|
__gm.state.select({ kind: 'asset', assetId: 'celly' });
|
||||||
__gm.state.openList({ kind: 'asset', assetId: 'celly', key: 'cells' });
|
__gm.state.openList({ kind: 'asset', assetId: 'celly', key: 'cells' });
|
||||||
`);
|
`);
|
||||||
await sleep(250);
|
|
||||||
info = await json('__gm.list.info()');
|
info = await json('__gm.list.info()');
|
||||||
check('a list of structs opens with a row per bundle', info.items.length === 2, JSON.stringify(info));
|
check('a list of structs opens with a row per bundle', info.items.length === 2, JSON.stringify(info));
|
||||||
check(
|
check(
|
||||||
@@ -296,7 +274,6 @@ export async function listTab(t) {
|
|||||||
await click(firstField.x, firstField.y);
|
await click(firstField.x, firstField.y);
|
||||||
await typeText('77');
|
await typeText('77');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(250);
|
|
||||||
const cells = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
const cells = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
||||||
check(
|
check(
|
||||||
'editing one field of one item leaves every other value untouched',
|
'editing one field of one item leaves every other value untouched',
|
||||||
@@ -308,7 +285,6 @@ export async function listTab(t) {
|
|||||||
JSON.stringify(cells),
|
JSON.stringify(cells),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
const back = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
const back = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
||||||
check(
|
check(
|
||||||
'and undo takes back just that field',
|
'and undo takes back just that field',
|
||||||
@@ -325,7 +301,6 @@ export async function listTab(t) {
|
|||||||
const cellsOf = () => json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
const cellsOf = () => json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
|
||||||
check('a struct item carries the add-a-field control', (await find('+ Field')) !== null);
|
check('a struct item carries the add-a-field control', (await find('+ Field')) !== null);
|
||||||
await clickText('+ Field');
|
await clickText('+ Field');
|
||||||
await sleep(150);
|
|
||||||
await pickKind('Boolean');
|
await pickKind('Boolean');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
check(
|
check(
|
||||||
@@ -356,7 +331,6 @@ export async function listTab(t) {
|
|||||||
await press('a', { ctrl: true });
|
await press('a', { ctrl: true });
|
||||||
await typeText('row');
|
await typeText('row');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'renaming a field of one item leaves the other item alone',
|
'renaming a field of one item leaves the other item alone',
|
||||||
JSON.stringify(await cellsOf()) ===
|
JSON.stringify(await cellsOf()) ===
|
||||||
@@ -368,7 +342,6 @@ export async function listTab(t) {
|
|||||||
);
|
);
|
||||||
const fieldMark = await json("__gm.list.markAt(0, 'x')");
|
const fieldMark = await json("__gm.list.markAt(0, 'x')");
|
||||||
await click(fieldMark.x, fieldMark.y);
|
await click(fieldMark.x, fieldMark.y);
|
||||||
await sleep(150);
|
|
||||||
await pickKind('Color');
|
await pickKind('Color');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
check(
|
check(
|
||||||
@@ -379,7 +352,6 @@ export async function listTab(t) {
|
|||||||
await undo();
|
await undo();
|
||||||
await undo();
|
await undo();
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'and three undos take the shape back to what it was',
|
'and three undos take the shape back to what it was',
|
||||||
JSON.stringify(await cellsOf()) ===
|
JSON.stringify(await cellsOf()) ===
|
||||||
@@ -399,16 +371,13 @@ export async function listTab(t) {
|
|||||||
__gm.state.select({ kind: 'node', assetId: 'celly', nodeId: 'counter', nodeIds: ['counter'] });
|
__gm.state.select({ kind: 'node', assetId: 'celly', nodeId: 'counter', nodeIds: ['counter'] });
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
const portButton = await find('0 items');
|
const portButton = await find('0 items');
|
||||||
check('an unconnected list input reads as an item count', portButton !== null);
|
check('an unconnected list input reads as an item count', portButton !== null);
|
||||||
await click(portButton.x, portButton.y);
|
await click(portButton.x, portButton.y);
|
||||||
await sleep(250);
|
|
||||||
check('clicking it opens the List tab on the port', (await json('__gm.tab()')) === 'list');
|
check('clicking it opens the List tab on the port', (await json('__gm.tab()')) === 'list');
|
||||||
info = await json('__gm.list.info()');
|
info = await json('__gm.list.info()');
|
||||||
check('titled by the node and the port it belongs to', info.title === 'Count · list', info.title);
|
check('titled by the node and the port it belongs to', info.title === 'Count · list', info.title);
|
||||||
await clickText('+ Item');
|
await clickText('+ Item');
|
||||||
await sleep(150);
|
|
||||||
await pickKind('Number');
|
await pickKind('Number');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
const params = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').graph.nodes[0].params`);
|
const params = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').graph.nodes[0].params`);
|
||||||
@@ -425,10 +394,8 @@ export async function listTab(t) {
|
|||||||
__gm.showTab('scene');
|
__gm.showTab('scene');
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
await mutate('__gm.state.openList(null)');
|
await mutate('__gm.state.openList(null)');
|
||||||
await sleep(200);
|
|
||||||
const left = await json(
|
const left = await json(
|
||||||
'__gm.state.project.assets.filter((a) => a.id === "listy" || a.id === "celly").length',
|
'__gm.state.project.assets.filter((a) => a.id === "listy" || a.id === "celly").length',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export async function structVariable(t) {
|
|||||||
});
|
});
|
||||||
__gm.state.select({ kind: 'asset', assetId: 'structy' });
|
__gm.state.select({ kind: 'asset', assetId: 'structy' });
|
||||||
`);
|
`);
|
||||||
await sleep(150);
|
|
||||||
|
|
||||||
// 22a. The declaration is what the inspector draws: a head that can add fields, a row per field.
|
// 22a. The declaration is what the inspector draws: a head that can add fields, a row per field.
|
||||||
check('a struct variable carries the add-a-field control', (await find('+ Field')) !== null);
|
check('a struct variable carries the add-a-field control', (await find('+ Field')) !== null);
|
||||||
@@ -62,7 +61,6 @@ export async function structVariable(t) {
|
|||||||
await click(xField.x, xField.y);
|
await click(xField.x, xField.y);
|
||||||
await typeText('42');
|
await typeText('42');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
check('typing in a field writes that field', (await piece()).x === 42, JSON.stringify(await piece()));
|
check('typing in a field writes that field', (await piece()).x === 42, JSON.stringify(await piece()));
|
||||||
|
|
||||||
const tagField = await json("__gm.inspectorField('tag')");
|
const tagField = await json("__gm.inspectorField('tag')");
|
||||||
@@ -70,7 +68,6 @@ export async function structVariable(t) {
|
|||||||
await press('a', { ctrl: true });
|
await press('a', { ctrl: true });
|
||||||
await typeText('ell');
|
await typeText('ell');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'and the next field edited keeps the one before it',
|
'and the next field edited keeps the one before it',
|
||||||
JSON.stringify(await piece()) === '{"x":42,"tag":"ell"}',
|
JSON.stringify(await piece()) === '{"x":42,"tag":"ell"}',
|
||||||
@@ -85,7 +82,6 @@ export async function structVariable(t) {
|
|||||||
|
|
||||||
// 22d. Adding a field reaches the declaration and the value at once.
|
// 22d. Adding a field reaches the declaration and the value at once.
|
||||||
await clickText('+ Field');
|
await clickText('+ Field');
|
||||||
await sleep(150);
|
|
||||||
await pickKind('Boolean');
|
await pickKind('Boolean');
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
const grown = await piece();
|
const grown = await piece();
|
||||||
@@ -93,7 +89,6 @@ export async function structVariable(t) {
|
|||||||
check('and the new field has a row', (await find('field1')) !== null);
|
check('and the new field has a row', (await find('field1')) !== null);
|
||||||
await shot('02-field-added');
|
await shot('02-field-added');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
check('undo takes it out of the value too', !('field1' in (await piece())), JSON.stringify(await piece()));
|
check('undo takes it out of the value too', !('field1' in (await piece())), JSON.stringify(await piece()));
|
||||||
|
|
||||||
// 22e. Renaming a field carries its value along.
|
// 22e. Renaming a field carries its value along.
|
||||||
@@ -102,7 +97,6 @@ export async function structVariable(t) {
|
|||||||
await press('a', { ctrl: true });
|
await press('a', { ctrl: true });
|
||||||
await typeText('column');
|
await typeText('column');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'renaming a field keeps its value',
|
'renaming a field keeps its value',
|
||||||
JSON.stringify(await piece()) === '{"column":30,"tag":"tee"}',
|
JSON.stringify(await piece()) === '{"column":30,"tag":"tee"}',
|
||||||
@@ -112,11 +106,9 @@ export async function structVariable(t) {
|
|||||||
// 22f. A field may itself be a list. Its row is the same "n items" button a list property has,
|
// 22f. A field may itself be a list. Its row is the same "n items" button a list property has,
|
||||||
// and it opens the List tab one level down – on the field, not on the whole bundle.
|
// and it opens the List tab one level down – on the field, not on the whole bundle.
|
||||||
await clickText('+ Field');
|
await clickText('+ Field');
|
||||||
await sleep(150);
|
|
||||||
// "List" is also the tab bar's own label, so the menu entry is reached as the last drawn one.
|
// "List" is also the tab bar's own label, so the menu entry is reached as the last drawn one.
|
||||||
const listOption = await json(`__gm.locate('List', true)`);
|
const listOption = await json(`__gm.locate('List', true)`);
|
||||||
await click(listOption.x, listOption.y);
|
await click(listOption.x, listOption.y);
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'+ Field ▸ List gives the bundle an empty list',
|
'+ Field ▸ List gives the bundle an empty list',
|
||||||
Array.isArray((await piece()).field1),
|
Array.isArray((await piece()).field1),
|
||||||
@@ -126,7 +118,6 @@ export async function structVariable(t) {
|
|||||||
check('and that field reads as an item count', itemsButton !== null);
|
check('and that field reads as an item count', itemsButton !== null);
|
||||||
|
|
||||||
await click(itemsButton.x, itemsButton.y);
|
await click(itemsButton.x, itemsButton.y);
|
||||||
await sleep(250);
|
|
||||||
check('clicking it opens the List tab', (await json('__gm.tab()')) === 'list');
|
check('clicking it opens the List tab', (await json('__gm.tab()')) === 'list');
|
||||||
const info = await json('__gm.list.info()');
|
const info = await json('__gm.list.info()');
|
||||||
check('whose title names the field inside the bundle', info.title.endsWith('· field1'), info.title);
|
check('whose title names the field inside the bundle', info.title.endsWith('· field1'), info.title);
|
||||||
@@ -134,7 +125,6 @@ export async function structVariable(t) {
|
|||||||
// The depth the stored value goes ends here: this list sits in a struct field, so a struct item
|
// The depth the stored value goes ends here: this list sits in a struct field, so a struct item
|
||||||
// would be dropped by the next write. It is left out of the menu rather than doing nothing.
|
// would be dropped by the next write. It is left out of the menu rather than doing nothing.
|
||||||
await clickText('+ Item');
|
await clickText('+ Item');
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'the menu of a list in a struct field leaves the struct kind out',
|
'the menu of a list in a struct field leaves the struct kind out',
|
||||||
(await find('Number')) !== null && (await find('Struct')) === null,
|
(await find('Number')) !== null && (await find('Struct')) === null,
|
||||||
@@ -150,7 +140,6 @@ export async function structVariable(t) {
|
|||||||
await shot('03-list-field');
|
await shot('03-list-field');
|
||||||
await undo();
|
await undo();
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'undo takes the list field back out of the bundle',
|
'undo takes the list field back out of the bundle',
|
||||||
!('field1' in (await piece())),
|
!('field1' in (await piece())),
|
||||||
@@ -158,7 +147,6 @@ export async function structVariable(t) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await mutate('__gm.state.openList(null)');
|
await mutate('__gm.state.openList(null)');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 22g. The same bundle on a placed object. An override is sparse – it starts out unset – while a
|
// 22g. The same bundle on a placed object. An override is sparse – it starts out unset – while a
|
||||||
// field edit rebuilds the whole bundle, so the untouched fields have to come from what the row
|
// field edit rebuilds the whole bundle, so the untouched fields have to come from what the row
|
||||||
@@ -177,7 +165,6 @@ export async function structVariable(t) {
|
|||||||
});
|
});
|
||||||
`);
|
`);
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'ostruct' });
|
await select({ kind: 'object', holderId: 'scene1', objectId: 'ostruct' });
|
||||||
await sleep(200);
|
|
||||||
check('a placed behavior shows its bundle, filled from the asset', (await find('tee')) !== null);
|
check('a placed behavior shows its bundle, filled from the asset', (await find('tee')) !== null);
|
||||||
|
|
||||||
// 22h. It reads exactly like the asset does: a head carrying the struct mark, then one row per
|
// 22h. It reads exactly like the asset does: a head carrying the struct mark, then one row per
|
||||||
@@ -236,7 +223,6 @@ export async function structVariable(t) {
|
|||||||
await press('a', { ctrl: true });
|
await press('a', { ctrl: true });
|
||||||
await typeText('7');
|
await typeText('7');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'the first override of one field keeps the asset values of the others',
|
'the first override of one field keeps the asset values of the others',
|
||||||
JSON.stringify(await placed()) === '{"column":7,"tag":"tee"}',
|
JSON.stringify(await placed()) === '{"column":7,"tag":"tee"}',
|
||||||
@@ -245,7 +231,6 @@ export async function structVariable(t) {
|
|||||||
await shot('04-object-override');
|
await shot('04-object-override');
|
||||||
|
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'and undo takes the override away again, not just the one field',
|
'and undo takes the override away again, not just the one field',
|
||||||
(await json("__gm.state.activeScene.objects.find((o) => o.id === 'ostruct').overrides.piece")) === null,
|
(await json("__gm.state.activeScene.objects.find((o) => o.id === 'ostruct').overrides.piece")) === null,
|
||||||
@@ -260,7 +245,6 @@ export async function structVariable(t) {
|
|||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
const left = await json(
|
const left = await json(
|
||||||
'__gm.state.project.assets.filter((a) => a.id === "structy").length +' +
|
'__gm.state.project.assets.filter((a) => a.id === "structy").length +' +
|
||||||
' __gm.state.activeScene.objects.filter((o) => o.id === "ostruct").length',
|
' __gm.state.activeScene.objects.filter((o) => o.id === "ostruct").length',
|
||||||
|
|||||||
@@ -35,44 +35,24 @@ const logicGraph = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function variableNodes(t) {
|
export async function variableNodes(t) {
|
||||||
const {
|
const { check, sleep, json, shot, click, locate, undo, entity, asset, mutate, stop, typeText, press, has } =
|
||||||
check,
|
t;
|
||||||
sleep,
|
|
||||||
evaluate,
|
|
||||||
json,
|
|
||||||
shot,
|
|
||||||
click,
|
|
||||||
locate,
|
|
||||||
undo,
|
|
||||||
entity,
|
|
||||||
asset,
|
|
||||||
mutate,
|
|
||||||
stop,
|
|
||||||
typeText,
|
|
||||||
press,
|
|
||||||
has,
|
|
||||||
} = t;
|
|
||||||
const portAt = (nodeId, key, isOutput) =>
|
const portAt = (nodeId, key, isOutput) =>
|
||||||
json(`__gm.graph.portAt(${JSON.stringify(nodeId)}, ${JSON.stringify(key)}, ${isOutput})`);
|
json(`__gm.graph.portAt(${JSON.stringify(nodeId)}, ${JSON.stringify(key)}, ${isOutput})`);
|
||||||
const portsOf = async (nodeId) =>
|
const portsOf = async (nodeId) =>
|
||||||
(await asset('vp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? [];
|
(await asset('vp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? [];
|
||||||
const select = async (nodeId) => {
|
const select = async (nodeId) => {
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: 'vp-logic', nodeId, nodeIds: [nodeId] });
|
||||||
`__gm.state.select({ kind: 'node', assetId: 'vp-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
|
|
||||||
);
|
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
/** The Set checkbox of the newest variable row: the control right of the last "Set" label. */
|
/** The Set checkbox of the newest variable row: the control right of the last "Set" label. */
|
||||||
const clickLastSet = async () => {
|
const clickLastSet = async () => {
|
||||||
const box = await json(`__gm.inspectorField('Set', true)`);
|
const box = await json(`__gm.inspectorField('Set', true)`);
|
||||||
await click(box.x, box.y);
|
await click(box.x, box.y);
|
||||||
await sleep(300);
|
|
||||||
return box;
|
return box;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,12 +73,10 @@ export async function variableNodes(t) {
|
|||||||
${MAKE_OBJECT}
|
${MAKE_OBJECT}
|
||||||
scene.objects.push(make('vp-box', 'crate', '', 900, 'Speedy box'), make('vp-brain', 'vp-logic', 'vp-box', 900, 'Speeds'));
|
scene.objects.push(make('vp-box', 'crate', '', 900, 'Speedy box'), make('vp-brain', 'vp-logic', 'vp-box', 900, 'Speeds'));
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 23b. The run: both variables are written by one node and read back by another, and the two
|
// 23b. The run: both variables are written by one node and read back by another, and the two
|
||||||
// speeds stay apart – 2 to the right for every 3 down.
|
// speeds stay apart – 2 to the right for every 3 down.
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(300);
|
|
||||||
const before = await entity('vp-box');
|
const before = await entity('vp-box');
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
const after = await entity('vp-box');
|
const after = await entity('vp-box');
|
||||||
@@ -110,7 +88,6 @@ export async function variableNodes(t) {
|
|||||||
`moved (${dx.toFixed(1)}, ${dy.toFixed(1)}) px in ~1 s – (200, 300) means both ports carried their own variable`,
|
`moved (${dx.toFixed(1)}, ${dy.toFixed(1)}) px in ~1 s – (200, 300) means both ports carried their own variable`,
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 23c. Every variable is an output; a written one is an input too, and only a writing node has
|
// 23c. Every variable is an output; a written one is an input too, and only a writing node has
|
||||||
// flow pins.
|
// flow pins.
|
||||||
@@ -134,9 +111,7 @@ export async function variableNodes(t) {
|
|||||||
check('the node offers its variables in the inspector', Boolean(add), JSON.stringify(add));
|
check('the node offers its variables in the inspector', Boolean(add), JSON.stringify(add));
|
||||||
await shot('01-variable-ports');
|
await shot('01-variable-ports');
|
||||||
await click(add.x, add.y);
|
await click(add.x, add.y);
|
||||||
await sleep(200);
|
|
||||||
await clickLast('vz');
|
await clickLast('vz');
|
||||||
await sleep(300);
|
|
||||||
const grown = await portsOf('vp-set');
|
const grown = await portsOf('vp-set');
|
||||||
check(
|
check(
|
||||||
'picking one adds it as a read port, typed from the variable',
|
'picking one adds it as a read port, typed from the variable',
|
||||||
@@ -161,18 +136,14 @@ export async function variableNodes(t) {
|
|||||||
);
|
);
|
||||||
await shot('02-variable-set');
|
await shot('02-variable-set');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check('undo takes both back', (await portsOf('vp-set')).length === 2);
|
check('undo takes both back', (await portsOf('vp-set')).length === 2);
|
||||||
|
|
||||||
// 23e. Pointing a port at another variable keeps the edge hanging on it – a correction, not a
|
// 23e. Pointing a port at another variable keeps the edge hanging on it – a correction, not a
|
||||||
// rewiring. `vp-e3` runs from this port to Move's dx.
|
// rewiring. `vp-e3` runs from this port to Move's dx.
|
||||||
await select('vp-get');
|
await select('vp-get');
|
||||||
await clickLast('vx');
|
await clickLast('vx');
|
||||||
await sleep(200);
|
|
||||||
await clickLast('vz');
|
await clickLast('vz');
|
||||||
await sleep(300);
|
|
||||||
const swapped = await portsOf('vp-get');
|
const swapped = await portsOf('vp-get');
|
||||||
const edgeStill = await json(
|
const edgeStill = await json(
|
||||||
`(__gm.state.project.assets.find((a) => a.id === 'vp-logic').graph.edges.find((e) => e.id === 'vp-e3') || {}).from`,
|
`(__gm.state.project.assets.find((a) => a.id === 'vp-logic').graph.edges.find((e) => e.id === 'vp-e3') || {}).from`,
|
||||||
@@ -183,7 +154,6 @@ export async function variableNodes(t) {
|
|||||||
JSON.stringify({ swapped, edgeStill }),
|
JSON.stringify({ swapped, edgeStill }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check('and undo points it back', (await portsOf('vp-get'))[0].key === 'vx');
|
check('and undo points it back', (await portsOf('vp-get'))[0].key === 'vx');
|
||||||
|
|
||||||
// 23f. Collapsed, the ports are out of sight, so the title says what the node does – and only then.
|
// 23f. Collapsed, the ports are out of sight, so the title says what the node does – and only then.
|
||||||
@@ -191,7 +161,6 @@ export async function variableNodes(t) {
|
|||||||
const toggle = async (id) => {
|
const toggle = async (id) => {
|
||||||
const chevron = await json(`__gm.graph.chevronAt(${JSON.stringify(id)})`);
|
const chevron = await json(`__gm.graph.chevronAt(${JSON.stringify(id)})`);
|
||||||
await click(chevron.x, chevron.y);
|
await click(chevron.x, chevron.y);
|
||||||
await sleep(250);
|
|
||||||
};
|
};
|
||||||
/** `locate` matches exactly, so a title cut short by a too-narrow node is not found. */
|
/** `locate` matches exactly, so a title cut short by a too-narrow node is not found. */
|
||||||
const drawnWhole = async (text) => {
|
const drawnWhole = async (text) => {
|
||||||
@@ -222,7 +191,6 @@ export async function variableNodes(t) {
|
|||||||
JSON.stringify({ switched, narrow }),
|
JSON.stringify({ switched, narrow }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
await toggle('vp-set');
|
await toggle('vp-set');
|
||||||
await toggle('vp-get');
|
await toggle('vp-get');
|
||||||
const open = [await titleOf('vp-set'), await titleOf('vp-get')];
|
const open = [await titleOf('vp-set'), await titleOf('vp-get')];
|
||||||
@@ -250,16 +218,13 @@ export async function variableNodes(t) {
|
|||||||
a.props.shapes = [{ x: 0, y: 0 }];
|
a.props.shapes = [{ x: 0, y: 0 }];
|
||||||
a.graph.nodes.push(${JSON.stringify(node('vp-pack', 'PackStruct', 560, 400))});
|
a.graph.nodes.push(${JSON.stringify(node('vp-pack', 'PackStruct', 560, 400))});
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
await select('vp-pack');
|
await select('vp-pack');
|
||||||
const from = await locate('From variable');
|
const from = await locate('From variable');
|
||||||
check('a Pack offers the shapes its behavior declares', Boolean(from), JSON.stringify(from));
|
check('a Pack offers the shapes its behavior declares', Boolean(from), JSON.stringify(from));
|
||||||
await click(from.x, from.y);
|
await click(from.x, from.y);
|
||||||
await sleep(200);
|
|
||||||
await shot('03-from-variable');
|
await shot('03-from-variable');
|
||||||
await clickLast('piece');
|
await clickLast('piece');
|
||||||
await sleep(300);
|
|
||||||
const seeded = await portsOf('vp-pack');
|
const seeded = await portsOf('vp-pack');
|
||||||
check(
|
check(
|
||||||
'picking a struct variable declares every field at once, each of its own kind',
|
'picking a struct variable declares every field at once, each of its own kind',
|
||||||
@@ -270,15 +235,12 @@ export async function variableNodes(t) {
|
|||||||
check('and the node grew those inputs', Boolean(await portAt('vp-pack', 'cells', false)));
|
check('and the node grew those inputs', Boolean(await portAt('vp-pack', 'cells', false)));
|
||||||
await shot('04-fields-seeded');
|
await shot('04-fields-seeded');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check('one undo takes all of them back', (await portsOf('vp-pack')).length === 0);
|
check('one undo takes all of them back', (await portsOf('vp-pack')).length === 0);
|
||||||
|
|
||||||
// 23h. A list declares nothing, so the shape comes off the struct items in it – the bundle a Pack
|
// 23h. A list declares nothing, so the shape comes off the struct items in it – the bundle a Pack
|
||||||
// builds for such a list.
|
// builds for such a list.
|
||||||
await click(from.x, from.y);
|
await click(from.x, from.y);
|
||||||
await sleep(200);
|
|
||||||
await clickLast('shapes (item)');
|
await clickLast('shapes (item)');
|
||||||
await sleep(300);
|
|
||||||
const fromItem = await portsOf('vp-pack');
|
const fromItem = await portsOf('vp-pack');
|
||||||
check(
|
check(
|
||||||
'a list offers the shape of the struct items it holds',
|
'a list offers the shape of the struct items it holds',
|
||||||
@@ -286,7 +248,6 @@ export async function variableNodes(t) {
|
|||||||
JSON.stringify(fromItem),
|
JSON.stringify(fromItem),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 23i. A behavior's own inputs are not fields of a bundle, so the helper stays out of that section.
|
// 23i. A behavior's own inputs are not fields of a bundle, so the helper stays out of that section.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
@@ -294,7 +255,6 @@ export async function variableNodes(t) {
|
|||||||
${JSON.stringify(node('vp-start', 'BehaviorStart', 560, 600))},
|
${JSON.stringify(node('vp-start', 'BehaviorStart', 560, 600))},
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
await select('vp-start');
|
await select('vp-start');
|
||||||
// `locate` throws when the text is absent, which is the wrong shape for "is it there?".
|
// `locate` throws when the text is absent, which is the wrong shape for "is it there?".
|
||||||
const helper = await json(`__gm.locate('From variable', false)`);
|
const helper = await json(`__gm.locate('From variable', false)`);
|
||||||
@@ -311,12 +271,9 @@ export async function variableNodes(t) {
|
|||||||
// Bare canvas below every node, so the click cannot land on the Pack.
|
// Bare canvas below every node, so the click cannot land on the Pack.
|
||||||
const bareSpot = await json('__gm.graph.emptySpot()');
|
const bareSpot = await json('__gm.graph.emptySpot()');
|
||||||
await click(bareSpot.x, bareSpot.y, 'right');
|
await click(bareSpot.x, bareSpot.y, 'right');
|
||||||
await sleep(150);
|
|
||||||
// Enter takes the first hit in category order, so search by a phrase only its description has.
|
// Enter takes the first hit in category order, so search by a phrase only its description has.
|
||||||
await typeText('switched to Set');
|
await typeText('switched to Set');
|
||||||
await sleep(200);
|
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(300);
|
|
||||||
const added = (await asset('vp-logic')).graph.nodes.find((n) => !idsBefore.includes(n.id));
|
const added = (await asset('vp-logic')).graph.nodes.find((n) => !idsBefore.includes(n.id));
|
||||||
check('the palette offers the Variables node', added?.type === 'Variables', JSON.stringify(added));
|
check('the palette offers the Variables node', added?.type === 'Variables', JSON.stringify(added));
|
||||||
const variableId = added?.id ?? '';
|
const variableId = added?.id ?? '';
|
||||||
@@ -324,7 +281,6 @@ export async function variableNodes(t) {
|
|||||||
const bare = !(await portAt(variableId, 'exec', false));
|
const bare = !(await portAt(variableId, 'exec', false));
|
||||||
await clickLast('+ Variable');
|
await clickLast('+ Variable');
|
||||||
await clickLast('vz');
|
await clickLast('vz');
|
||||||
await sleep(300);
|
|
||||||
await clickLastSet();
|
await clickLastSet();
|
||||||
check(
|
check(
|
||||||
'it starts without pins, and picking its variable leaves the title Variables',
|
'it starts without pins, and picking its variable leaves the title Variables',
|
||||||
@@ -346,7 +302,6 @@ export async function variableNodes(t) {
|
|||||||
${JSON.stringify(edge('vp-e9', 'vp-add', 'result', variableId, 'vz'))},
|
${JSON.stringify(edge('vp-e9', 'vp-add', 'result', variableId, 'vz'))},
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
await select('vp-gone');
|
await select('vp-gone');
|
||||||
check(
|
check(
|
||||||
'a port naming a variable the behavior does not have is marked in the inspector',
|
'a port naming a variable the behavior does not have is marked in the inspector',
|
||||||
@@ -357,7 +312,6 @@ export async function variableNodes(t) {
|
|||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
const counted = (await entity('vp-brain'))?.props.vz;
|
const counted = (await entity('vp-brain'))?.props.vz;
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'in the run it reads through its own output and writes the sum back, so vz climbs',
|
'in the run it reads through its own output and writes the sum back, so vz climbs',
|
||||||
typeof counted === 'number' && counted > 20,
|
typeof counted === 'number' && counted > 20,
|
||||||
@@ -372,7 +326,6 @@ export async function variableNodes(t) {
|
|||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
const left = await json('__gm.state.project.assets.filter((a) => a.id === "vp-logic").length');
|
const left = await json('__gm.state.project.assets.filter((a) => a.id === "vp-logic").length');
|
||||||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ const logicGraph = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function propertyNodes(t) {
|
export async function propertyNodes(t) {
|
||||||
const { check, sleep, evaluate, json, shot, click, doubleClick, locate, find, undo, entity, asset } = t;
|
const { check, evaluate, json, shot, click, doubleClick, locate, find, undo, entity, asset } = t;
|
||||||
const { mutate, stop, typeText, press } = t;
|
const { mutate, stop, typeText, press } = t;
|
||||||
const portsOf = async (nodeId) =>
|
const portsOf = async (nodeId) =>
|
||||||
((await asset('pp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? []).map(
|
((await asset('pp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? []).map(
|
||||||
@@ -38,44 +38,34 @@ export async function propertyNodes(t) {
|
|||||||
const title = (nodeId) => json(`__gm.graph.nodeLabel(${JSON.stringify(nodeId)})`);
|
const title = (nodeId) => json(`__gm.graph.nodeLabel(${JSON.stringify(nodeId)})`);
|
||||||
const settings = async (nodeId) => (await json(`__gm.graph.nodeSettings(${JSON.stringify(nodeId)})`)) ?? [];
|
const settings = async (nodeId) => (await json(`__gm.graph.nodeSettings(${JSON.stringify(nodeId)})`)) ?? [];
|
||||||
const select = async (nodeId) => {
|
const select = async (nodeId) => {
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: 'pp-logic', nodeId, nodeIds: [nodeId] });
|
||||||
`__gm.state.select({ kind: 'node', assetId: 'pp-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
|
|
||||||
);
|
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
/** A port section takes its choices when it is built, and re-selecting the same node is a no-op. */
|
/** A port section takes its choices when it is built, and re-selecting the same node is a no-op. */
|
||||||
const rebuildInspector = async () => {
|
const rebuildInspector = async () => {
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(150);
|
|
||||||
await select('pp-set');
|
await select('pp-set');
|
||||||
};
|
};
|
||||||
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
const offered = async (shotName) => {
|
const offered = async (shotName) => {
|
||||||
await clickLast('+ Property');
|
await clickLast('+ Property');
|
||||||
const texts = await json('__gm.overlayTexts()');
|
const texts = await json('__gm.overlayTexts()');
|
||||||
if (shotName) await shot(shotName);
|
if (shotName) await shot(shotName);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(200);
|
|
||||||
return texts;
|
return texts;
|
||||||
};
|
};
|
||||||
const addProperty = async (search) => {
|
const addProperty = async (search) => {
|
||||||
await clickLast('+ Property');
|
await clickLast('+ Property');
|
||||||
await typeText(search);
|
await typeText(search);
|
||||||
await sleep(150);
|
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
const choose = async (setting, option) => {
|
const choose = async (setting, option) => {
|
||||||
const field = await json(`__gm.inspectorField(${JSON.stringify(setting)}, true)`);
|
const field = await json(`__gm.inspectorField(${JSON.stringify(setting)}, true)`);
|
||||||
await click(field.x, field.y);
|
await click(field.x, field.y);
|
||||||
await sleep(200);
|
|
||||||
await clickLast(option);
|
await clickLast(option);
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
/** The name field, not the node's port or the unconnected-input row below it that say the same. */
|
/** The name field, not the node's port or the unconnected-input row below it that say the same. */
|
||||||
const rename = async (from, to) => {
|
const rename = async (from, to) => {
|
||||||
@@ -85,7 +75,6 @@ export async function propertyNodes(t) {
|
|||||||
await doubleClick(field.x, field.y);
|
await doubleClick(field.x, field.y);
|
||||||
await typeText(to);
|
await typeText(to);
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 24a. A box wearing a behavior that reshapes it through one node.
|
// 24a. A box wearing a behavior that reshapes it through one node.
|
||||||
@@ -104,15 +93,12 @@ export async function propertyNodes(t) {
|
|||||||
${MAKE_OBJECT}
|
${MAKE_OBJECT}
|
||||||
scene.objects.push(make('pp-box', 'crate', '', 900, 'Reshaped box'), make('pp-brain', 'pp-logic', 'pp-box', 900, 'Reshape'));
|
scene.objects.push(make('pp-box', 'crate', '', 900, 'Reshaped box'), make('pp-brain', 'pp-logic', 'pp-box', 900, 'Reshape'));
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 24b. The run: one node writes both properties, and its own output reads the width back after.
|
// 24b. The run: one node writes both properties, and its own output reads the width back after.
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(400);
|
|
||||||
const box = (await entity('pp-box'))?.props;
|
const box = (await entity('pp-box'))?.props;
|
||||||
const seen = (await entity('pp-brain'))?.props.seen;
|
const seen = (await entity('pp-brain'))?.props.seen;
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'one node writes several properties and reads them back',
|
'one node writes several properties and reads them back',
|
||||||
box?.width === 40 && box?.color === 0xff0000 && seen === 40,
|
box?.width === 40 && box?.color === 0xff0000 && seen === 40,
|
||||||
@@ -204,7 +190,6 @@ export async function propertyNodes(t) {
|
|||||||
);
|
);
|
||||||
const toggle = await json(`__gm.inspectorField('Set', true)`);
|
const toggle = await json(`__gm.inspectorField('Set', true)`);
|
||||||
await click(toggle.x, toggle.y);
|
await click(toggle.x, toggle.y);
|
||||||
await sleep(300);
|
|
||||||
const inputs = await json(`__gm.graph.nodeInputs('pp-set')`);
|
const inputs = await json(`__gm.graph.nodeInputs('pp-set')`);
|
||||||
check(
|
check(
|
||||||
"switched to Set, its typed-in value starts at the property's own default",
|
"switched to Set, its typed-in value starts at the property's own default",
|
||||||
@@ -214,7 +199,6 @@ export async function propertyNodes(t) {
|
|||||||
await shot('03-height-set');
|
await shot('03-height-set');
|
||||||
await undo();
|
await undo();
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check('undo takes both back', (await portsOf('pp-set')).length === 2, (await portsOf('pp-set')).join());
|
check('undo takes both back', (await portsOf('pp-set')).length === 2, (await portsOf('pp-set')).join());
|
||||||
|
|
||||||
// 24f. Another type offers its own fields; a port it does not have is marked, not dropped.
|
// 24f. Another type offers its own fields; a port it does not have is marked, not dropped.
|
||||||
@@ -234,7 +218,6 @@ export async function propertyNodes(t) {
|
|||||||
);
|
);
|
||||||
await shot('04-camera');
|
await shot('04-camera');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));
|
check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));
|
||||||
|
|
||||||
// 24g. Any names its ports by hand, and the kind is picked when adding one. A name that happens
|
// 24g. Any names its ports by hand, and the kind is picked when adding one. A name that happens
|
||||||
@@ -294,7 +277,6 @@ export async function propertyNodes(t) {
|
|||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
const left = await json('__gm.state.project.assets.filter((a) => a.id === "pp-logic").length');
|
const left = await json('__gm.state.project.assets.filter((a) => a.id === "pp-logic").length');
|
||||||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,8 @@ const logicGraph = (walkId) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export async function animation(t) {
|
export async function animation(t) {
|
||||||
const { check, sleep, json, evaluate, shot, mutate, press, click, clickText, drag, undo, redo } = t;
|
const { check, sleep, json, evaluate, shot, mutate, press, click, clickText, drag, undo, redo, showTab } =
|
||||||
|
t;
|
||||||
const { mouse, typeText } = t;
|
const { mouse, typeText } = t;
|
||||||
const { entity, stop } = t;
|
const { entity, stop } = t;
|
||||||
const info = () => json('__gm.animation.current()');
|
const info = () => json('__gm.animation.current()');
|
||||||
@@ -77,7 +78,6 @@ export async function animation(t) {
|
|||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 25a. A strip of four cells, cut into four frames and moved to the head of the asset list.
|
// 25a. A strip of four cells, cut into four frames and moved to the head of the asset list.
|
||||||
await evaluate(PASTE_STRIP);
|
await evaluate(PASTE_STRIP);
|
||||||
@@ -94,14 +94,11 @@ export async function animation(t) {
|
|||||||
texture.frames = ${JSON.stringify(FRAME_IDS)}.map((id, i) => ({ id, name: 'Cell ' + (i + 1), x: i * 16, y: 0, w: 16, h: 16 }));
|
texture.frames = ${JSON.stringify(FRAME_IDS)}.map((id, i) => ({ id, name: 'Cell ' + (i + 1), x: i * 16, y: 0, w: 16, h: 16 }));
|
||||||
p.assets.unshift(texture);
|
p.assets.unshift(texture);
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 25b. The texture's context menu makes an animation of its frames, as one undo step.
|
// 25b. The texture's context menu makes an animation of its frames, as one undo step.
|
||||||
const row = await json(`__gm.assetRow(${JSON.stringify(strip)})`);
|
const row = await json(`__gm.assetRow(${JSON.stringify(strip)})`);
|
||||||
await click(row.x, row.y, 'right');
|
await click(row.x, row.y, 'right');
|
||||||
await sleep(300);
|
|
||||||
await clickText('Create animation from texture');
|
await clickText('Create animation from texture');
|
||||||
await sleep(400);
|
|
||||||
const made = await json(
|
const made = await json(
|
||||||
"__gm.state.project.assets.filter((a) => a.type === 'animation').map((a) => ({ id: a.id, name: a.name, frames: a.frames, props: a.props }))",
|
"__gm.state.project.assets.filter((a) => a.type === 'animation').map((a) => ({ id: a.id, name: a.name, frames: a.frames, props: a.props }))",
|
||||||
);
|
);
|
||||||
@@ -120,10 +117,8 @@ export async function animation(t) {
|
|||||||
JSON.stringify({ selection: await json('__gm.state.selection'), walk }),
|
JSON.stringify({ selection: await json('__gm.state.selection'), walk }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
const undone = await json("__gm.state.project.assets.filter((a) => a.type === 'animation').length");
|
const undone = await json("__gm.state.project.assets.filter((a) => a.type === 'animation').length");
|
||||||
await redo();
|
await redo();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'undo takes asset and frames back in one step, redo brings both',
|
'undo takes asset and frames back in one step, redo brings both',
|
||||||
undone === 0 && JSON.stringify(await frames(walk.id)) === JSON.stringify(FRAME_IDS),
|
undone === 0 && JSON.stringify(await frames(walk.id)) === JSON.stringify(FRAME_IDS),
|
||||||
@@ -133,7 +128,6 @@ export async function animation(t) {
|
|||||||
// 25c. The Animation tab shows it and plays it; the inspector keeps its two properties.
|
// 25c. The Animation tab shows it and plays it; the inspector keeps its two properties.
|
||||||
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(walk.id)} })`);
|
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(walk.id)} })`);
|
||||||
await openTab(t, 'animation');
|
await openTab(t, 'animation');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the inspector edits frames per second and playback',
|
'the inspector edits frames per second and playback',
|
||||||
(await t.has('Frames per second')) && (await t.has('Playback')) && (await t.has('Loop')),
|
(await t.has('Frames per second')) && (await t.has('Playback')) && (await t.has('Loop')),
|
||||||
@@ -184,7 +178,6 @@ export async function animation(t) {
|
|||||||
|
|
||||||
// 25d. Pausing holds the frame on screen, and the preview shows that frame's picture.
|
// 25d. Pausing holds the frame on screen, and the preview shows that frame's picture.
|
||||||
await click(...Object.values(await json('__gm.animation.playButton()')));
|
await click(...Object.values(await json('__gm.animation.playButton()')));
|
||||||
await sleep(200);
|
|
||||||
const paused = await info();
|
const paused = await info();
|
||||||
await sleep(350);
|
await sleep(350);
|
||||||
check(
|
check(
|
||||||
@@ -204,7 +197,6 @@ export async function animation(t) {
|
|||||||
const tile = async (i) => json(`__gm.animation.tileCentre(${i})`);
|
const tile = async (i) => json(`__gm.animation.tileCentre(${i})`);
|
||||||
const third = await tile(2);
|
const third = await tile(2);
|
||||||
await click(third.x, third.y);
|
await click(third.x, third.y);
|
||||||
await sleep(250);
|
|
||||||
const picked = await info();
|
const picked = await info();
|
||||||
check(
|
check(
|
||||||
'clicking a tile selects it and the preview shows it',
|
'clicking a tile selects it and the preview shows it',
|
||||||
@@ -234,7 +226,6 @@ export async function animation(t) {
|
|||||||
nearestCell(await json(`__gm.pixelAt(${pointer.x}, ${pointer.y})`)) === 'blue',
|
nearestCell(await json(`__gm.pixelAt(${pointer.x}, ${pointer.y})`)) === 'blue',
|
||||||
);
|
);
|
||||||
await drag(pointer.x, pointer.y, pointer.x + 40, pointer.y - 30, { button: 'middle' });
|
await drag(pointer.x, pointer.y, pointer.x + 40, pointer.y - 30, { button: 'middle' });
|
||||||
await sleep(200);
|
|
||||||
const panned = (await info()).preview;
|
const panned = (await info()).preview;
|
||||||
check(
|
check(
|
||||||
'the middle button pans it',
|
'the middle button pans it',
|
||||||
@@ -251,7 +242,6 @@ export async function animation(t) {
|
|||||||
);
|
);
|
||||||
await shot('02b-zoomed');
|
await shot('02b-zoomed');
|
||||||
await clickText('Fit');
|
await clickText('Fit');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'Fit puts the preview back',
|
'Fit puts the preview back',
|
||||||
JSON.stringify((await info()).preview) === JSON.stringify(fitted),
|
JSON.stringify((await info()).preview) === JSON.stringify(fitted),
|
||||||
@@ -261,7 +251,6 @@ export async function animation(t) {
|
|||||||
// 25f. Dragging a tile into another gap reorders the frames, as one undo step.
|
// 25f. Dragging a tile into another gap reorders the frames, as one undo step.
|
||||||
const first = await tile(0);
|
const first = await tile(0);
|
||||||
await drag(first.x, first.y, third.x + 39, third.y, 10);
|
await drag(first.x, first.y, third.x + 39, third.y, 10);
|
||||||
await sleep(300);
|
|
||||||
const reordered = await frames(walk.id);
|
const reordered = await frames(walk.id);
|
||||||
check(
|
check(
|
||||||
'dragging the first tile behind the third moves that frame there',
|
'dragging the first tile behind the third moves that frame there',
|
||||||
@@ -270,15 +259,12 @@ export async function animation(t) {
|
|||||||
JSON.stringify({ reordered, info: await info() }),
|
JSON.stringify({ reordered, info: await info() }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(250);
|
|
||||||
check('undo puts it back', JSON.stringify(await frames(walk.id)) === JSON.stringify(FRAME_IDS));
|
check('undo puts it back', JSON.stringify(await frames(walk.id)) === JSON.stringify(FRAME_IDS));
|
||||||
|
|
||||||
// 25g. Delete removes the selected tile.
|
// 25g. Delete removes the selected tile.
|
||||||
const second = await tile(1);
|
const second = await tile(1);
|
||||||
await click(second.x, second.y);
|
await click(second.x, second.y);
|
||||||
await sleep(200);
|
|
||||||
await press('Delete');
|
await press('Delete');
|
||||||
await sleep(250);
|
|
||||||
const deleted = await frames(walk.id);
|
const deleted = await frames(walk.id);
|
||||||
check(
|
check(
|
||||||
'Delete removes the selected frame and nothing else',
|
'Delete removes the selected frame and nothing else',
|
||||||
@@ -287,24 +273,23 @@ export async function animation(t) {
|
|||||||
JSON.stringify(deleted),
|
JSON.stringify(deleted),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(250);
|
|
||||||
|
|
||||||
// 25h. + Frames offers a sheet's frames at once and inserts them after the selection.
|
// 25h. + Frames offers a sheet's frames at once and inserts them after the selection.
|
||||||
await clickText('+ Frames');
|
await clickText('+ Frames');
|
||||||
await sleep(300);
|
// Search first: in the full suite a 17-frame sheet sorts ahead of this one and pushes the later
|
||||||
|
// cells out of the list's view, and a row outside its list is not on the screen to be read.
|
||||||
|
// The filter also sharpens the check, because the animation is called "strip 2" and would match.
|
||||||
|
await typeText('strip');
|
||||||
const offered = await json('__gm.overlayTexts()');
|
const offered = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'+ Frames offers the whole strip and each cell, but no animation',
|
'+ Frames offers the whole strip and each cell, but no animation',
|
||||||
offered.includes('strip – all 4 frames') &&
|
offered.includes('strip – all 4 frames') &&
|
||||||
offered.includes('strip / Cell 3') &&
|
offered.includes('strip / Cell 3') &&
|
||||||
|
offered.includes('strip / Cell 4') &&
|
||||||
!offered.some((s) => s.startsWith('strip 2')),
|
!offered.some((s) => s.startsWith('strip 2')),
|
||||||
JSON.stringify(offered),
|
JSON.stringify(offered),
|
||||||
);
|
);
|
||||||
// In the full suite a 17-frame sheet sorts first and pushes this row out of the list's view.
|
|
||||||
await typeText('strip');
|
|
||||||
await sleep(150);
|
|
||||||
await clickText('strip – all 4 frames');
|
await clickText('strip – all 4 frames');
|
||||||
await sleep(300);
|
|
||||||
const grown = await frames(walk.id);
|
const grown = await frames(walk.id);
|
||||||
check(
|
check(
|
||||||
'all four go in right after the selected frame',
|
'all four go in right after the selected frame',
|
||||||
@@ -312,7 +297,6 @@ export async function animation(t) {
|
|||||||
JSON.stringify(grown),
|
JSON.stringify(grown),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(250);
|
|
||||||
|
|
||||||
// 25i. A sprite wears the animation through its Texture picker.
|
// 25i. A sprite wears the animation through its Texture picker.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
@@ -331,7 +315,6 @@ export async function animation(t) {
|
|||||||
await sleep(500);
|
await sleep(500);
|
||||||
const picker = await json("__gm.inspectorField('Texture', true)");
|
const picker = await json("__gm.inspectorField('Texture', true)");
|
||||||
await click(picker.x, picker.y);
|
await click(picker.x, picker.y);
|
||||||
await sleep(300);
|
|
||||||
const choices = await json('__gm.overlayTexts()');
|
const choices = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'the Texture picker offers the animation first, marked as one',
|
'the Texture picker offers the animation first, marked as one',
|
||||||
@@ -339,14 +322,13 @@ export async function animation(t) {
|
|||||||
JSON.stringify(choices),
|
JSON.stringify(choices),
|
||||||
);
|
);
|
||||||
await clickText('strip 2 (animation)');
|
await clickText('strip 2 (animation)');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'picking it makes the sprite wear the animation',
|
'picking it makes the sprite wear the animation',
|
||||||
(await json("__gm.state.project.assets.find((a) => a.id === 'an-sprite').props.texture")) === walk.id,
|
(await json("__gm.state.project.assets.find((a) => a.id === 'an-sprite').props.texture")) === walk.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 25j. The scene shows the first frame and does not play.
|
// 25j. The scene shows the first frame and does not play.
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(700);
|
await sleep(700);
|
||||||
const box = await json("__gm.objectRect('an-box')");
|
const box = await json("__gm.objectRect('an-box')");
|
||||||
const sceneColour = async () =>
|
const sceneColour = async () =>
|
||||||
@@ -374,22 +356,18 @@ export async function animation(t) {
|
|||||||
// lies outside; `outward` says which way the neighbouring cell would be.
|
// lies outside; `outward` says which way the neighbouring cell would be.
|
||||||
const edgePixels = async (side, target, outward) => {
|
const edgePixels = async (side, target, outward) => {
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(300);
|
|
||||||
const first = await edgesOf();
|
const first = await edgesOf();
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(200);
|
|
||||||
const shift = (target - fraction(first[side]) + 1) % 1;
|
const shift = (target - fraction(first[side]) + 1) % 1;
|
||||||
await mutate(
|
await mutate(
|
||||||
`__gm.state.activeScene.objects.find((o) => o.id === 'an-box').position.x += ${shift / first.scale}`,
|
`__gm.state.activeScene.objects.find((o) => o.id === 'an-box').position.x += ${shift / first.scale}`,
|
||||||
);
|
);
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(300);
|
|
||||||
const e = await edgesOf();
|
const e = await edgesOf();
|
||||||
const x = Math.floor(e[side]);
|
const x = Math.floor(e[side]);
|
||||||
const edge = rgb(await json(`__gm.pixelAt(${x}, ${e.y})`));
|
const edge = rgb(await json(`__gm.pixelAt(${x}, ${e.y})`));
|
||||||
const outside = rgb(await json(`__gm.pixelAt(${x + outward * 2}, ${e.y})`));
|
const outside = rgb(await json(`__gm.pixelAt(${x + outward * 2}, ${e.y})`));
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(200);
|
|
||||||
return { fraction: fraction(e[side]).toFixed(2), edge, outside };
|
return { fraction: fraction(e[side]).toFixed(2), edge, outside };
|
||||||
};
|
};
|
||||||
// The edge pixel is the cell's green over the background and nothing else: red and blue rise
|
// The edge pixel is the cell's green over the background and nothing else: red and blue rise
|
||||||
@@ -420,11 +398,9 @@ export async function animation(t) {
|
|||||||
await mutate(
|
await mutate(
|
||||||
`__gm.state.project.assets.find((a) => a.id === 'an-sprite').props.texture = ${JSON.stringify(walk.id)}`,
|
`__gm.state.project.assets.find((a) => a.id === 'an-sprite').props.texture = ${JSON.stringify(walk.id)}`,
|
||||||
);
|
);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 25k. Play runs it: the pixels the game draws change colour over time.
|
// 25k. Play runs it: the pixels the game draws change colour over time.
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(150);
|
|
||||||
const colours = new Set();
|
const colours = new Set();
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const at = await json("__gm.gameScreen('an-box')");
|
const at = await json("__gm.gameScreen('an-box')");
|
||||||
@@ -434,7 +410,6 @@ export async function animation(t) {
|
|||||||
const clock = (await entity('an-box'))?.animation;
|
const clock = (await entity('an-box'))?.animation;
|
||||||
await shot('04-playing');
|
await shot('04-playing');
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'in play the sprite runs through the frames',
|
'in play the sprite runs through the frames',
|
||||||
[...colours].filter((c) => NAMES.includes(c)).length >= 3 && clock?.id === walk.id && clock.time > 0,
|
[...colours].filter((c) => NAMES.includes(c)).length >= 3 && clock?.id === walk.id && clock.time > 0,
|
||||||
@@ -460,7 +435,6 @@ export async function animation(t) {
|
|||||||
scene.objects.push(make('an-brain', 'an-logic', 'an-box', 0, 'Animation logic'));
|
scene.objects.push(make('an-brain', 'an-logic', 'an-box', 0, 'Animation logic'));
|
||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(900);
|
await sleep(900);
|
||||||
const brain = (await entity('an-brain'))?.props;
|
const brain = (await entity('an-brain'))?.props;
|
||||||
@@ -470,7 +444,6 @@ export async function animation(t) {
|
|||||||
? nearestCell(await json(`__gm.pixelAt(${Math.round(at.x)}, ${Math.round(at.y)})`))
|
? nearestCell(await json(`__gm.pixelAt(${Math.round(at.x)}, ${Math.round(at.y)})`))
|
||||||
: 'none';
|
: 'none';
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'On animation finished fires once when a once animation ends',
|
'On animation finished fires once when a once animation ends',
|
||||||
brain?.done === 1,
|
brain?.done === 1,
|
||||||
@@ -487,11 +460,8 @@ export async function animation(t) {
|
|||||||
const pickPicture = async (label, name) => {
|
const pickPicture = async (label, name) => {
|
||||||
const field = await json(`__gm.inspectorField(${JSON.stringify(label)}, true)`);
|
const field = await json(`__gm.inspectorField(${JSON.stringify(label)}, true)`);
|
||||||
await click(field.x, field.y);
|
await click(field.x, field.y);
|
||||||
await sleep(200);
|
|
||||||
await typeText(name);
|
await typeText(name);
|
||||||
await sleep(150);
|
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const logic = __gm.state.project.assets.find((a) => a.id === 'an-logic');
|
const logic = __gm.state.project.assets.find((a) => a.id === 'an-logic');
|
||||||
@@ -500,7 +470,6 @@ export async function animation(t) {
|
|||||||
logic.props.pic = '';
|
logic.props.pic = '';
|
||||||
__gm.state.select({ kind: 'node', assetId: 'an-logic', nodeId: 'an-pic', nodeIds: ['an-pic'] });
|
__gm.state.select({ kind: 'node', assetId: 'an-logic', nodeId: 'an-pic', nodeIds: ['an-pic'] });
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
await pickPicture('Value', walk.name);
|
await pickPicture('Value', walk.name);
|
||||||
const picNode = (await t.asset('an-logic')).graph.nodes.find((n) => n.id === 'an-pic');
|
const picNode = (await t.asset('an-logic')).graph.nodes.find((n) => n.id === 'an-pic');
|
||||||
const picRows = (await json("__gm.graph.nodeSettings('an-pic')")) ?? [];
|
const picRows = (await json("__gm.graph.nodeSettings('an-pic')")) ?? [];
|
||||||
@@ -510,7 +479,6 @@ export async function animation(t) {
|
|||||||
JSON.stringify({ params: picNode?.params, rows: picRows }),
|
JSON.stringify({ params: picNode?.params, rows: picRows }),
|
||||||
);
|
);
|
||||||
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'an-logic' })");
|
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'an-logic' })");
|
||||||
await sleep(400);
|
|
||||||
await pickPicture('pic', walk.name);
|
await pickPicture('pic', walk.name);
|
||||||
check(
|
check(
|
||||||
'a texture variable is picked with the same picker',
|
'a texture variable is picked with the same picker',
|
||||||
@@ -522,9 +490,7 @@ export async function animation(t) {
|
|||||||
// 25m. Deleting the texture counts the animations that play its frames.
|
// 25m. Deleting the texture counts the animations that play its frames.
|
||||||
const again = await json(`__gm.assetRow(${JSON.stringify(strip)})`);
|
const again = await json(`__gm.assetRow(${JSON.stringify(strip)})`);
|
||||||
await click(again.x, again.y, 'right');
|
await click(again.x, again.y, 'right');
|
||||||
await sleep(300);
|
|
||||||
await clickText('Delete…');
|
await clickText('Delete…');
|
||||||
await sleep(300);
|
|
||||||
const dialog = await json('__gm.overlayTexts()');
|
const dialog = await json('__gm.overlayTexts()');
|
||||||
await shot('05-delete-dialog');
|
await shot('05-delete-dialog');
|
||||||
check(
|
check(
|
||||||
@@ -535,7 +501,6 @@ export async function animation(t) {
|
|||||||
check('and its bold Delete button is not cut short', dialog.includes('Delete'), JSON.stringify(dialog));
|
check('and its bold Delete button is not cut short', dialog.includes('Delete'), JSON.stringify(dialog));
|
||||||
const cancel = await t.locate('Cancel', true);
|
const cancel = await t.locate('Cancel', true);
|
||||||
await click(cancel.x, cancel.y);
|
await click(cancel.x, cancel.y);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const p = __gm.state.project;
|
const p = __gm.state.project;
|
||||||
@@ -546,7 +511,6 @@ export async function animation(t) {
|
|||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
__gm.state.preview();
|
__gm.state.preview();
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
const left = await json(
|
const left = await json(
|
||||||
"__gm.state.project.assets.filter((a) => a.type === 'animation' || a.id === 'an-sprite' || a.id === 'an-logic').length",
|
"__gm.state.project.assets.filter((a) => a.type === 'animation' || a.id === 'an-sprite' || a.id === 'an-logic').length",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ export async function stateMachine(t) {
|
|||||||
doubleClick,
|
doubleClick,
|
||||||
drag,
|
drag,
|
||||||
mouse,
|
mouse,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
const { typeText, undo, redo, world, play, stop } = t;
|
const { settle, typeText, undo, redo, world, play, stop } = t;
|
||||||
const machines = () =>
|
const machines = () =>
|
||||||
json(`__gm.state.project.assets
|
json(`__gm.state.project.assets
|
||||||
.filter((a) => a.type === 'statemachine')
|
.filter((a) => a.type === 'statemachine')
|
||||||
@@ -34,29 +35,22 @@ export async function stateMachine(t) {
|
|||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
const stateLabel = (id) => json(`__gm.stateMachine.nodeLabel(${JSON.stringify(id)})`);
|
const stateLabel = (id) => json(`__gm.stateMachine.nodeLabel(${JSON.stringify(id)})`);
|
||||||
const selection = () => json('__gm.state.selection');
|
const selection = () => json('__gm.state.selection');
|
||||||
const outputsOf = async (nodeId) => (await json(`__gm.graph.nodeOutputs(${JSON.stringify(nodeId)})`)) ?? [];
|
const outputsOf = async (nodeId) => (await json(`__gm.graph.nodeOutputs(${JSON.stringify(nodeId)})`)) ?? [];
|
||||||
const inputsOf = async (nodeId) => (await json(`__gm.graph.nodeInputs(${JSON.stringify(nodeId)})`)) ?? [];
|
const inputsOf = async (nodeId) => (await json(`__gm.graph.nodeInputs(${JSON.stringify(nodeId)})`)) ?? [];
|
||||||
const selectNode = async (nodeId) => {
|
const selectNode = async (nodeId) => {
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: 'sm-logic', nodeId, nodeIds: [nodeId] });
|
||||||
`__gm.state.select({ kind: 'node', assetId: 'sm-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
|
|
||||||
);
|
|
||||||
await sleep(400);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 26a. The + menu creates a state machine; selected, it opens its own tab.
|
// 26a. The + menu creates a state machine; selected, it opens its own tab.
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await sleep(200);
|
|
||||||
await clickLast('State machine');
|
await clickLast('State machine');
|
||||||
await sleep(400);
|
|
||||||
const made = (await machines())[0];
|
const made = (await machines())[0];
|
||||||
check(
|
check(
|
||||||
'the + menu creates a state machine asset',
|
'the + menu creates a state machine asset',
|
||||||
@@ -86,9 +80,7 @@ export async function stateMachine(t) {
|
|||||||
const canvas = await json('__gm.stateMachine.canvasRect()');
|
const canvas = await json('__gm.stateMachine.canvasRect()');
|
||||||
const addState = async (dy) => {
|
const addState = async (dy) => {
|
||||||
await click(canvas.x + 80, canvas.y + dy, 'right');
|
await click(canvas.x + 80, canvas.y + dy, 'right');
|
||||||
await sleep(300);
|
|
||||||
await clickLast('State');
|
await clickLast('State');
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
await addState(60);
|
await addState(60);
|
||||||
await addState(220);
|
await addState(220);
|
||||||
@@ -122,7 +114,6 @@ export async function stateMachine(t) {
|
|||||||
const preSnap = JSON.stringify(await states());
|
const preSnap = JSON.stringify(await states());
|
||||||
// The checkbox sits right of the "Snap" label; locate() gives the label's text centre.
|
// The checkbox sits right of the "Snap" label; locate() gives the label's text centre.
|
||||||
await click(snapLabel.x + 30, snapLabel.y);
|
await click(snapLabel.x + 30, snapLabel.y);
|
||||||
await sleep(150);
|
|
||||||
check(
|
check(
|
||||||
'the Snap toggle switches it on without moving a state',
|
'the Snap toggle switches it on without moving a state',
|
||||||
(await json('__gm.stateMachine.snapping()')) === true && JSON.stringify(await states()) === preSnap,
|
(await json('__gm.stateMachine.snapping()')) === true && JSON.stringify(await states()) === preSnap,
|
||||||
@@ -133,7 +124,6 @@ export async function stateMachine(t) {
|
|||||||
const from = await json(`__gm.stateMachine.nodeAt(${JSON.stringify(dragged)})`);
|
const from = await json(`__gm.stateMachine.nodeAt(${JSON.stringify(dragged)})`);
|
||||||
const wasAt = await stateXY(dragged);
|
const wasAt = await stateXY(dragged);
|
||||||
await drag(from.x, from.y, from.x + 50, from.y + 50);
|
await drag(from.x, from.y, from.x + 50, from.y + 50);
|
||||||
await sleep(200);
|
|
||||||
const nowAt = await stateXY(dragged);
|
const nowAt = await stateXY(dragged);
|
||||||
check(
|
check(
|
||||||
'a dragged state lands on the grid, not where the pointer let go',
|
'a dragged state lands on the grid, not where the pointer let go',
|
||||||
@@ -148,7 +138,6 @@ export async function stateMachine(t) {
|
|||||||
`${backAt.x},${backAt.y}`,
|
`${backAt.x},${backAt.y}`,
|
||||||
);
|
);
|
||||||
await click(snapLabel.x + 30, snapLabel.y);
|
await click(snapLabel.x + 30, snapLabel.y);
|
||||||
await sleep(150);
|
|
||||||
check('the toggle switches snapping off again', (await json('__gm.stateMachine.snapping()')) === false);
|
check('the toggle switches snapping off again', (await json('__gm.stateMachine.snapping()')) === false);
|
||||||
|
|
||||||
// 26c. The inspector renames the selected state; the node follows.
|
// 26c. The inspector renames the selected state; the node follows.
|
||||||
@@ -157,7 +146,6 @@ export async function stateMachine(t) {
|
|||||||
await doubleClick(nameField.x, nameField.y);
|
await doubleClick(nameField.x, nameField.y);
|
||||||
await typeText('Open');
|
await typeText('Open');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(300);
|
|
||||||
const renamed = (await states())[1];
|
const renamed = (await states())[1];
|
||||||
check(
|
check(
|
||||||
'typing in the Name field renames the state, on the node too',
|
'typing in the Name field renames the state, on the node too',
|
||||||
@@ -181,7 +169,6 @@ export async function stateMachine(t) {
|
|||||||
|
|
||||||
// 26e. The canvas palette offers a state and nothing else.
|
// 26e. The canvas palette offers a state and nothing else.
|
||||||
await click(canvas.x + canvas.w - 40, canvas.y + canvas.h - 40, 'right');
|
await click(canvas.x + canvas.w - 40, canvas.y + canvas.h - 40, 'right');
|
||||||
await sleep(300);
|
|
||||||
const offered = await json('__gm.overlayTexts()');
|
const offered = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'right-click offers a State and no behavior node',
|
'right-click offers a State and no behavior node',
|
||||||
@@ -189,7 +176,6 @@ export async function stateMachine(t) {
|
|||||||
JSON.stringify(offered),
|
JSON.stringify(offered),
|
||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 26e2. A transition is an ordinary edge, but it hangs on ports the editor never draws: a
|
// 26e2. A transition is an ordinary edge, but it hangs on ports the editor never draws: a
|
||||||
// transition joins two whole states, so there is no socket to aim at and none to grab.
|
// transition joins two whole states, so there is no socket to aim at and none to grab.
|
||||||
@@ -217,25 +203,20 @@ export async function stateMachine(t) {
|
|||||||
const makeTransition = async (fromId, toId) => {
|
const makeTransition = async (fromId, toId) => {
|
||||||
const from = await centreOfState(fromId);
|
const from = await centreOfState(fromId);
|
||||||
await click(from.x, from.y, 'right');
|
await click(from.x, from.y, 'right');
|
||||||
await sleep(300);
|
|
||||||
await clickLast('Make transition');
|
await clickLast('Make transition');
|
||||||
await sleep(250);
|
|
||||||
const to = await centreOfState(toId);
|
const to = await centreOfState(toId);
|
||||||
await click(to.x, to.y);
|
await click(to.x, to.y);
|
||||||
await sleep(350);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The right-click way in, since there is no pin to drag from any more.
|
// The right-click way in, since there is no pin to drag from any more.
|
||||||
const firstCentre = await centreOfState(first);
|
const firstCentre = await centreOfState(first);
|
||||||
await click(firstCentre.x, firstCentre.y, 'right');
|
await click(firstCentre.x, firstCentre.y, 'right');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
"a state's menu offers Make transition",
|
"a state's menu offers Make transition",
|
||||||
(await json('__gm.overlayTexts()')).includes('Make transition'),
|
(await json('__gm.overlayTexts()')).includes('Make transition'),
|
||||||
JSON.stringify(await json('__gm.overlayTexts()')),
|
JSON.stringify(await json('__gm.overlayTexts()')),
|
||||||
);
|
);
|
||||||
await clickLast('Make transition');
|
await clickLast('Make transition');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'armed, the canvas says which click it is waiting for',
|
'armed, the canvas says which click it is waiting for',
|
||||||
(await json('__gm.stateMachine.hint()')).includes('Click the state'),
|
(await json('__gm.stateMachine.hint()')).includes('Click the state'),
|
||||||
@@ -247,7 +228,6 @@ export async function stateMachine(t) {
|
|||||||
);
|
);
|
||||||
await shot('05a-armed');
|
await shot('05a-armed');
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(250);
|
|
||||||
check(
|
check(
|
||||||
'Escape drops the attempt, drawing nothing',
|
'Escape drops the attempt, drawing nothing',
|
||||||
(await transitions()).length === 0 && (await json('__gm.stateMachine.danglingEdge()')) === false,
|
(await transitions()).length === 0 && (await json('__gm.stateMachine.danglingEdge()')) === false,
|
||||||
@@ -265,25 +245,20 @@ export async function stateMachine(t) {
|
|||||||
(await json(`__gm.stateMachine.edgeDrawn(${JSON.stringify(drawn[0]?.id)})`)) === true,
|
(await json(`__gm.stateMachine.edgeDrawn(${JSON.stringify(drawn[0]?.id)})`)) === true,
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check('undo takes the transition back', (await transitions()).length === 0);
|
check('undo takes the transition back', (await transitions()).length === 0);
|
||||||
await redo();
|
await redo();
|
||||||
await sleep(300);
|
|
||||||
check('redo puts it back', (await transitions()).length === 1);
|
check('redo puts it back', (await transitions()).length === 1);
|
||||||
await shot('05-transition');
|
await shot('05-transition');
|
||||||
|
|
||||||
// 26e3. Naming a transition, from the list the inspector shows on the state it leaves.
|
// 26e3. Naming a transition, from the list the inspector shows on the state it leaves.
|
||||||
const edgeId = (await transitions())[0]?.id;
|
const edgeId = (await transitions())[0]?.id;
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: made.id, nodeId: first, nodeIds: [first] });
|
||||||
`__gm.state.select({ kind: 'node', assetId: ${JSON.stringify(made.id)}, nodeId: ${JSON.stringify(first)}, nodeIds: [${JSON.stringify(first)}] })`,
|
|
||||||
);
|
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'the inspector lists the transitions of the selected state, both ways round',
|
'the inspector lists the transitions of the selected state, both ways round',
|
||||||
(await has('TRANSITIONS OUT')) && (await has('TRANSITIONS IN')),
|
(await has('TRANSITIONS OUT')) && (await has('TRANSITIONS IN')),
|
||||||
);
|
);
|
||||||
await evaluate(`__gm.stateMachine.nameTransition(${JSON.stringify(edgeId)}, 'alarm')`);
|
await evaluate(`__gm.stateMachine.nameTransition(${JSON.stringify(edgeId)}, 'alarm')`);
|
||||||
await sleep(400);
|
await settle();
|
||||||
check(
|
check(
|
||||||
'a named transition carries the name on the edge itself',
|
'a named transition carries the name on the edge itself',
|
||||||
(await transitions())[0]?.label === 'alarm',
|
(await transitions())[0]?.label === 'alarm',
|
||||||
@@ -297,17 +272,15 @@ export async function stateMachine(t) {
|
|||||||
const field = await locate('alarm', true);
|
const field = await locate('alarm', true);
|
||||||
// Double-click, so the name is selected and replaced – a single click only parks the caret in it.
|
// Double-click, so the name is selected and replaced – a single click only parks the caret in it.
|
||||||
await doubleClick(field.x, field.y);
|
await doubleClick(field.x, field.y);
|
||||||
await sleep(200);
|
|
||||||
await typeText('lost');
|
await typeText('lost');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'and the panel row is a real field: typing in it renames the transition',
|
'and the panel row is a real field: typing in it renames the transition',
|
||||||
(await transitions())[0]?.label === 'lost',
|
(await transitions())[0]?.label === 'lost',
|
||||||
JSON.stringify(await transitions()),
|
JSON.stringify(await transitions()),
|
||||||
);
|
);
|
||||||
await evaluate(`__gm.stateMachine.nameTransition(${JSON.stringify(edgeId)}, 'alarm')`);
|
await evaluate(`__gm.stateMachine.nameTransition(${JSON.stringify(edgeId)}, 'alarm')`);
|
||||||
await sleep(300);
|
await settle();
|
||||||
await shot('05b-named');
|
await shot('05b-named');
|
||||||
|
|
||||||
// 26e4. Both ways round between the same pair: the two arrows must not lie on top of each other.
|
// 26e4. Both ways round between the same pair: the two arrows must not lie on top of each other.
|
||||||
@@ -320,7 +293,6 @@ export async function stateMachine(t) {
|
|||||||
check('both arrows are stroked', drawnBoth.every(Boolean), JSON.stringify(drawnBoth));
|
check('both arrows are stroked', drawnBoth.every(Boolean), JSON.stringify(drawnBoth));
|
||||||
await shot('05c-both-ways');
|
await shot('05c-both-ways');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 26f. A behavior reads the machine: Set state on start, State branch every frame.
|
// 26f. A behavior reads the machine: Set state on start, State branch every frame.
|
||||||
const rig = await scaffoldBehavior(t, {
|
const rig = await scaffoldBehavior(t, {
|
||||||
@@ -389,7 +361,6 @@ export async function stateMachine(t) {
|
|||||||
id: 'sm-e5', from: { node: 'sm-set', port: 'exec' }, to: { node: 'sm-next', port: 'exec' },
|
id: 'sm-e5', from: { node: 'sm-set', port: 'exec' }, to: { node: 'sm-next', port: 'exec' },
|
||||||
});
|
});
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
await sleep(600);
|
await sleep(600);
|
||||||
const moved = (await world())?.log ?? [];
|
const moved = (await world())?.log ?? [];
|
||||||
@@ -405,7 +376,6 @@ export async function stateMachine(t) {
|
|||||||
const b = __gm.state.project.assets.find((a) => a.id === 'sm-logic');
|
const b = __gm.state.project.assets.find((a) => a.id === 'sm-logic');
|
||||||
b.graph.nodes.find((n) => n.id === 'sm-next').params.transition = 'no such transition';
|
b.graph.nodes.find((n) => n.id === 'sm-next').params.transition = 'no such transition';
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
await sleep(600);
|
await sleep(600);
|
||||||
const stayed = (await world())?.log ?? [];
|
const stayed = (await world())?.log ?? [];
|
||||||
@@ -419,20 +389,16 @@ export async function stateMachine(t) {
|
|||||||
const b = __gm.state.project.assets.find((a) => a.id === 'sm-logic');
|
const b = __gm.state.project.assets.find((a) => a.id === 'sm-logic');
|
||||||
b.graph.nodes.find((n) => n.id === 'sm-next').params.transition = 'alarm';
|
b.graph.nodes.find((n) => n.id === 'sm-next').params.transition = 'alarm';
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 26f3. A start state: marked from the state's own menu, worn as a ring, entered by every
|
// 26f3. A start state: marked from the state's own menu, worn as a ring, entered by every
|
||||||
// instance before anything reads the machine – and heard as `On state entered`.
|
// instance before anything reads the machine – and heard as `On state entered`.
|
||||||
await evaluate("__gm.showTab('statemachine')");
|
await showTab('statemachine');
|
||||||
await sleep(300);
|
|
||||||
const startOf = () =>
|
const startOf = () =>
|
||||||
json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(made.id)}).start ?? ''`);
|
json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(made.id)}).start ?? ''`);
|
||||||
const ringOn = (id) => json(`__gm.stateMachine.startRing(${JSON.stringify(id)})`);
|
const ringOn = (id) => json(`__gm.stateMachine.startRing(${JSON.stringify(id)})`);
|
||||||
const marks = () => json('__gm.stateMachine.stateMarks()');
|
const marks = () => json('__gm.stateMachine.stateMarks()');
|
||||||
const startCentre = await centreOfState(first);
|
const startCentre = await centreOfState(first);
|
||||||
await click(startCentre.x, startCentre.y, 'right');
|
await click(startCentre.x, startCentre.y, 'right');
|
||||||
await sleep(300);
|
|
||||||
const stateMenu = await json('__gm.overlayTexts()');
|
const stateMenu = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'a state offers to become the start state',
|
'a state offers to become the start state',
|
||||||
@@ -440,7 +406,6 @@ export async function stateMachine(t) {
|
|||||||
JSON.stringify(stateMenu),
|
JSON.stringify(stateMenu),
|
||||||
);
|
);
|
||||||
await clickLast('Set as start state');
|
await clickLast('Set as start state');
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'the mark is kept on the asset and drawn as a ring on that state alone',
|
'the mark is kept on the asset and drawn as a ring on that state alone',
|
||||||
(await startOf()) === first && (await ringOn(first)) === true && (await ringOn(second)) === false,
|
(await startOf()) === first && (await ringOn(first)) === true && (await ringOn(second)) === false,
|
||||||
@@ -458,7 +423,6 @@ export async function stateMachine(t) {
|
|||||||
);
|
);
|
||||||
b.graph.edges.push(${JSON.stringify(edge('sm-e6', 'sm-entered', first, 'sm-entered-log', 'exec'))});
|
b.graph.edges.push(${JSON.stringify(edge('sm-e6', 'sm-entered', first, 'sm-entered-log', 'exec'))});
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
await sleep(600);
|
await sleep(600);
|
||||||
const started = (await world())?.log ?? [];
|
const started = (await world())?.log ?? [];
|
||||||
@@ -474,9 +438,8 @@ export async function stateMachine(t) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 26f4. The tab shows where the run is: weakly for any instance, strongly for the selected one.
|
// 26f4. The tab shows where the run is: weakly for any instance, strongly for the selected one.
|
||||||
await evaluate("__gm.showTab('statemachine')");
|
await showTab('statemachine');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(400);
|
|
||||||
const loose = await marks();
|
const loose = await marks();
|
||||||
check(
|
check(
|
||||||
'while playing, the state the machine is in is marked',
|
'while playing, the state the machine is in is marked',
|
||||||
@@ -486,7 +449,6 @@ export async function stateMachine(t) {
|
|||||||
await evaluate(
|
await evaluate(
|
||||||
"__gm.state.select({ kind: 'object', holderId: __gm.state.activeScene.id, objectId: 'sm-box', objectIds: ['sm-box'] })",
|
"__gm.state.select({ kind: 'object', holderId: __gm.state.activeScene.id, objectId: 'sm-box', objectIds: ['sm-box'] })",
|
||||||
);
|
);
|
||||||
await sleep(400);
|
|
||||||
const picked2 = await marks();
|
const picked2 = await marks();
|
||||||
check(
|
check(
|
||||||
"selecting the object makes its instance's state the strong one",
|
"selecting the object makes its instance's state the strong one",
|
||||||
@@ -495,8 +457,7 @@ export async function stateMachine(t) {
|
|||||||
);
|
);
|
||||||
await shot('06-live-state');
|
await shot('06-live-state');
|
||||||
await stop();
|
await stop();
|
||||||
await evaluate("__gm.showTab('statemachine')");
|
await showTab('statemachine');
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'nothing is marked once the run is over',
|
'nothing is marked once the run is over',
|
||||||
Object.keys(await marks()).length === 0,
|
Object.keys(await marks()).length === 0,
|
||||||
@@ -505,10 +466,8 @@ export async function stateMachine(t) {
|
|||||||
|
|
||||||
// 26f5. The same entry takes the mark off again, and the machine is back to starting nowhere.
|
// 26f5. The same entry takes the mark off again, and the machine is back to starting nowhere.
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(200);
|
|
||||||
const againCentre = await centreOfState(first);
|
const againCentre = await centreOfState(first);
|
||||||
await click(againCentre.x, againCentre.y, 'right');
|
await click(againCentre.x, againCentre.y, 'right');
|
||||||
await sleep(300);
|
|
||||||
const onMenu = await json('__gm.overlayTexts()');
|
const onMenu = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'the same place offers to take the mark off again',
|
'the same place offers to take the mark off again',
|
||||||
@@ -516,7 +475,6 @@ export async function stateMachine(t) {
|
|||||||
JSON.stringify(onMenu),
|
JSON.stringify(onMenu),
|
||||||
);
|
);
|
||||||
await clickLast('Remove start state');
|
await clickLast('Remove start state');
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'the mark and its ring are gone',
|
'the mark and its ring are gone',
|
||||||
(await startOf()) === '' && (await ringOn(first)) === false,
|
(await startOf()) === '' && (await ringOn(first)) === false,
|
||||||
@@ -538,7 +496,6 @@ export async function stateMachine(t) {
|
|||||||
b.graph.edges = b.graph.edges.filter((e) => e.id !== 'sm-e6');
|
b.graph.edges = b.graph.edges.filter((e) => e.id !== 'sm-e6');
|
||||||
b.graph.edges.push(${JSON.stringify(edge('sm-e1', 'sm-start', 'exec', 'sm-set', first))});
|
b.graph.edges.push(${JSON.stringify(edge('sm-e1', 'sm-start', 'exec', 'sm-set', first))});
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 26g. An unconnected machine input shows a picker; picking a machine grows the state ports.
|
// 26g. An unconnected machine input shows a picker; picking a machine grows the state ports.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
@@ -550,7 +507,6 @@ export async function stateMachine(t) {
|
|||||||
// The row is named after the port, as the node body names it.
|
// The row is named after the port, as the node body names it.
|
||||||
const machineField = await json("__gm.inspectorField('machine', true)");
|
const machineField = await json("__gm.inspectorField('machine', true)");
|
||||||
await click(machineField.x, machineField.y);
|
await click(machineField.x, machineField.y);
|
||||||
await sleep(300);
|
|
||||||
const choices = await json('__gm.overlayTexts()');
|
const choices = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'the Machine picker offers the machine by name',
|
'the Machine picker offers the machine by name',
|
||||||
@@ -558,7 +514,6 @@ export async function stateMachine(t) {
|
|||||||
JSON.stringify(choices),
|
JSON.stringify(choices),
|
||||||
);
|
);
|
||||||
await clickLast(made.name);
|
await clickLast(made.name);
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'picking it grows the state outputs',
|
'picking it grows the state outputs',
|
||||||
names(await outputsOf('sm-fresh')).includes('Open'),
|
names(await outputsOf('sm-fresh')).includes('Open'),
|
||||||
@@ -577,7 +532,6 @@ export async function stateMachine(t) {
|
|||||||
const m = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
const m = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||||
m.graph.nodes = m.graph.nodes.filter((n) => n.id !== ${JSON.stringify(second)});
|
m.graph.nodes = m.graph.nodes.filter((n) => n.id !== ${JSON.stringify(second)});
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
const ghosted = await outputsOf('sm-branch');
|
const ghosted = await outputsOf('sm-branch');
|
||||||
check(
|
check(
|
||||||
'the deleted state leaves a row named after its key, and takes its name away',
|
'the deleted state leaves a row named after its key, and takes its name away',
|
||||||
@@ -632,7 +586,6 @@ export async function stateMachine(t) {
|
|||||||
p.assets = p.assets.filter((a) => a.id !== ${JSON.stringify(made.id)});
|
p.assets = p.assets.filter((a) => a.id !== ${JSON.stringify(made.id)});
|
||||||
`);
|
`);
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing behind',
|
'the step leaves nothing behind',
|
||||||
left.objects === 0 && left.assets === 0 && (await machines()).length === 0,
|
left.objects === 0 && left.assets === 0 && (await machines()).length === 0,
|
||||||
|
|||||||
+6
-67
@@ -28,6 +28,7 @@ export async function shader(t) {
|
|||||||
play,
|
play,
|
||||||
stop,
|
stop,
|
||||||
shot,
|
shot,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
const { doubleClick, typeText } = t;
|
const { doubleClick, typeText } = t;
|
||||||
const field = (label) => json(`__gm.inspectorField(${JSON.stringify(label)})`);
|
const field = (label) => json(`__gm.inspectorField(${JSON.stringify(label)})`);
|
||||||
@@ -40,7 +41,6 @@ export async function shader(t) {
|
|||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
const assetProps = (patch) =>
|
const assetProps = (patch) =>
|
||||||
mutate(
|
mutate(
|
||||||
@@ -50,7 +50,6 @@ export async function shader(t) {
|
|||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 28a. The fixture ships Adjust colors as an asset; the + menu makes another and opens its tab on it.
|
// 28a. The fixture ships Adjust colors as an asset; the + menu makes another and opens its tab on it.
|
||||||
const seeded = (await shaders()).find((s) => s.id === ADJUST);
|
const seeded = (await shaders()).find((s) => s.id === ADJUST);
|
||||||
@@ -60,9 +59,7 @@ export async function shader(t) {
|
|||||||
JSON.stringify(seeded),
|
JSON.stringify(seeded),
|
||||||
);
|
);
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await sleep(200);
|
|
||||||
await clickLast('Shader');
|
await clickLast('Shader');
|
||||||
await sleep(400);
|
|
||||||
const made = (await shaders()).find((s) => s.id !== ADJUST);
|
const made = (await shaders()).find((s) => s.id !== ADJUST);
|
||||||
const selection = await json('__gm.state.selection');
|
const selection = await json('__gm.state.selection');
|
||||||
check(
|
check(
|
||||||
@@ -84,7 +81,6 @@ export async function shader(t) {
|
|||||||
// 28b. Right-click offers shader nodes only; Texture color and Output placed from the palette.
|
// 28b. Right-click offers shader nodes only; Texture color and Output placed from the palette.
|
||||||
const canvas = await json('__gm.shader.canvasRect()');
|
const canvas = await json('__gm.shader.canvasRect()');
|
||||||
await click(canvas.x + 80, canvas.y + 60, 'right');
|
await click(canvas.x + 80, canvas.y + 60, 'right');
|
||||||
await sleep(300);
|
|
||||||
const offered = await json('__gm.overlayTexts()');
|
const offered = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'right-click offers shader nodes and no behavior node',
|
'right-click offers shader nodes and no behavior node',
|
||||||
@@ -95,13 +91,9 @@ export async function shader(t) {
|
|||||||
);
|
);
|
||||||
// A dozen entries scroll; searched, the row is the only one and in view.
|
// A dozen entries scroll; searched, the row is the only one and in view.
|
||||||
await typeText('Texture color');
|
await typeText('Texture color');
|
||||||
await sleep(150);
|
|
||||||
await clickLast('Texture color');
|
await clickLast('Texture color');
|
||||||
await sleep(300);
|
|
||||||
await click(canvas.x + 420, canvas.y + 60, 'right');
|
await click(canvas.x + 420, canvas.y + 60, 'right');
|
||||||
await sleep(300);
|
|
||||||
await clickLast('Output');
|
await clickLast('Output');
|
||||||
await sleep(300);
|
|
||||||
const madeNodes = async () => (await shaders()).find((s) => s.id === made.id)?.nodes ?? [];
|
const madeNodes = async () => (await shaders()).find((s) => s.id === made.id)?.nodes ?? [];
|
||||||
check(
|
check(
|
||||||
'both nodes are in the graph, the Output where its right-click was',
|
'both nodes are in the graph, the Output where its right-click was',
|
||||||
@@ -109,7 +101,6 @@ export async function shader(t) {
|
|||||||
JSON.stringify(await madeNodes()),
|
JSON.stringify(await madeNodes()),
|
||||||
);
|
);
|
||||||
await click(canvas.x + 420, canvas.y + 320, 'right');
|
await click(canvas.x + 420, canvas.y + 320, 'right');
|
||||||
await sleep(300);
|
|
||||||
const again = await json('__gm.overlayTexts()');
|
const again = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'the palette offers the Output only once',
|
'the palette offers the Output only once',
|
||||||
@@ -117,7 +108,6 @@ export async function shader(t) {
|
|||||||
JSON.stringify(again),
|
JSON.stringify(again),
|
||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'an Output with nothing connected compiles: the texel passes through, no property declared',
|
'an Output with nothing connected compiles: the texel passes through, no property declared',
|
||||||
(await info()) === '0 properties',
|
(await info()) === '0 properties',
|
||||||
@@ -126,12 +116,9 @@ export async function shader(t) {
|
|||||||
|
|
||||||
// 28c. A Property node: its name is the row a sprite shows; a bad name is a problem on the node.
|
// 28c. A Property node: its name is the row a sprite shows; a bad name is a problem on the node.
|
||||||
await click(canvas.x + 80, canvas.y + 320, 'right');
|
await click(canvas.x + 80, canvas.y + 320, 'right');
|
||||||
await sleep(300);
|
|
||||||
// Property is the twelfth entry, below the palette's scroll mask: search it into view first.
|
// Property is the twelfth entry, below the palette's scroll mask: search it into view first.
|
||||||
await typeText('Property');
|
await typeText('Property');
|
||||||
await sleep(150);
|
|
||||||
await clickLast('Property');
|
await clickLast('Property');
|
||||||
await sleep(300);
|
|
||||||
const propertyNode = (await madeNodes()).find((n) => n.type === 'ShaderProperty');
|
const propertyNode = (await madeNodes()).find((n) => n.type === 'ShaderProperty');
|
||||||
check(
|
check(
|
||||||
'the placed property is named "value" and counted',
|
'the placed property is named "value" and counted',
|
||||||
@@ -143,7 +130,6 @@ export async function shader(t) {
|
|||||||
await doubleClick(nameField.x, nameField.y);
|
await doubleClick(nameField.x, nameField.y);
|
||||||
await typeText('glow');
|
await typeText('glow');
|
||||||
await press('Enter');
|
await press('Enter');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'typing in the Name field renames the property, on the node too',
|
'typing in the Name field renames the property, on the node too',
|
||||||
(await madeNodes()).find((n) => n.id === propertyNode?.id)?.name === 'glow' &&
|
(await madeNodes()).find((n) => n.id === propertyNode?.id)?.name === 'glow' &&
|
||||||
@@ -154,7 +140,6 @@ export async function shader(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||||
a.graph.nodes.find((n) => n.id === ${JSON.stringify(propertyNode?.id)}).params.name = 'no good';`);
|
a.graph.nodes.find((n) => n.id === ${JSON.stringify(propertyNode?.id)}).params.name = 'no good';`);
|
||||||
await sleep(300);
|
|
||||||
const problem = await json(`__gm.shader.nodeProblem(${JSON.stringify(propertyNode?.id)})`);
|
const problem = await json(`__gm.shader.nodeProblem(${JSON.stringify(propertyNode?.id)})`);
|
||||||
check(
|
check(
|
||||||
'a name that is not one word is a problem on the node and the reason in the readout',
|
'a name that is not one word is a problem on the node and the reason in the readout',
|
||||||
@@ -164,7 +149,6 @@ export async function shader(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||||
a.graph.nodes.find((n) => n.id === ${JSON.stringify(propertyNode?.id)}).params.name = 'glow';`);
|
a.graph.nodes.find((n) => n.id === ${JSON.stringify(propertyNode?.id)}).params.name = 'glow';`);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 28c-ii. Where a node sits is the editor's business: dropping it somewhere else compiles nothing
|
// 28c-ii. Where a node sits is the editor's business: dropping it somewhere else compiles nothing
|
||||||
// again, a changed param does – once (#65).
|
// again, a changed param does – once (#65).
|
||||||
@@ -172,7 +156,6 @@ export async function shader(t) {
|
|||||||
const compiledBefore = await json('__gm.shaderCompiles()');
|
const compiledBefore = await json('__gm.shaderCompiles()');
|
||||||
const grab = await json(`__gm.shader.nodeAt(${nodeRef})`);
|
const grab = await json(`__gm.shader.nodeAt(${nodeRef})`);
|
||||||
await drag(grab.x, grab.y, grab.x + 60, grab.y + 40);
|
await drag(grab.x, grab.y, grab.x + 60, grab.y + 40);
|
||||||
await sleep(300);
|
|
||||||
const dropped = await json(`__gm.shader.nodeAt(${nodeRef})`);
|
const dropped = await json(`__gm.shader.nodeAt(${nodeRef})`);
|
||||||
const compiledAfterMove = await json('__gm.shaderCompiles()');
|
const compiledAfterMove = await json('__gm.shaderCompiles()');
|
||||||
check(
|
check(
|
||||||
@@ -188,7 +171,6 @@ export async function shader(t) {
|
|||||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||||
a.graph.nodes.find((n) => n.id === ${nodeRef}).params.default = ${JSON.stringify(value)};`);
|
a.graph.nodes.find((n) => n.id === ${nodeRef}).params.default = ${JSON.stringify(value)};`);
|
||||||
await setDefault(2);
|
await setDefault(2);
|
||||||
await sleep(300);
|
|
||||||
const compiledAfterParam = await json('__gm.shaderCompiles()');
|
const compiledAfterParam = await json('__gm.shaderCompiles()');
|
||||||
check(
|
check(
|
||||||
'changing a param compiles it again, once',
|
'changing a param compiles it again, once',
|
||||||
@@ -196,7 +178,6 @@ export async function shader(t) {
|
|||||||
JSON.stringify({ compiledAfterMove, compiledAfterParam }),
|
JSON.stringify({ compiledAfterMove, compiledAfterParam }),
|
||||||
);
|
);
|
||||||
await setDefault(defaultBefore);
|
await setDefault(defaultBefore);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 28d. A flat sprite on the camera, drawn on top of everything; its Shader dropdown lists the assets.
|
// 28d. A flat sprite on the camera, drawn on top of everything; its Shader dropdown lists the assets.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
@@ -212,9 +193,8 @@ export async function shader(t) {
|
|||||||
__gm.state.select(null)`);
|
__gm.state.select(null)`);
|
||||||
await sleep(600);
|
await sleep(600);
|
||||||
// The pixels are the scene view's; the tab is still the shader's.
|
// The pixels are the scene view's; the tab is still the shader's.
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectIds: ['oshade'], objectId: 'oshade' });
|
await select({ kind: 'object', holderId: 'scene1', objectIds: ['oshade'], objectId: 'oshade' });
|
||||||
await sleep(400);
|
|
||||||
const box = await json("__gm.objectRect('oshade')");
|
const box = await json("__gm.objectRect('oshade')");
|
||||||
check('the shaded sprite is on screen', Boolean(box) && box.w > 8, JSON.stringify(box));
|
check('the shaded sprite is on screen', Boolean(box) && box.w > 8, JSON.stringify(box));
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
@@ -245,7 +225,6 @@ export async function shader(t) {
|
|||||||
JSON.stringify(shaderField),
|
JSON.stringify(shaderField),
|
||||||
);
|
);
|
||||||
await click(shaderField.x, shaderField.y);
|
await click(shaderField.x, shaderField.y);
|
||||||
await sleep(300);
|
|
||||||
const choices = await json('__gm.overlayTexts()');
|
const choices = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'the dropdown offers None and every shader asset',
|
'the dropdown offers None and every shader asset',
|
||||||
@@ -255,7 +234,6 @@ export async function shader(t) {
|
|||||||
JSON.stringify({ choices, shaderField }),
|
JSON.stringify({ choices, shaderField }),
|
||||||
);
|
);
|
||||||
await clickLast(choices.find((text) => text.includes('Adjust colors')));
|
await clickLast(choices.find((text) => text.includes('Adjust colors')));
|
||||||
await sleep(400);
|
|
||||||
const rows = await Promise.all([field('brightness'), field('contrast'), field('saturation')]);
|
const rows = await Promise.all([field('brightness'), field('contrast'), field('saturation')]);
|
||||||
check(
|
check(
|
||||||
'picking Adjust colors adds its three property rows below the dropdown',
|
'picking Adjust colors adds its three property rows below the dropdown',
|
||||||
@@ -266,14 +244,11 @@ export async function shader(t) {
|
|||||||
);
|
);
|
||||||
await shot('28-shader-rows');
|
await shot('28-shader-rows');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
|
||||||
check('undoing the choice takes the rows away again', (await field('brightness')) === null);
|
check('undoing the choice takes the rows away again', (await field('brightness')) === null);
|
||||||
await select(null);
|
await select(null);
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 28e. Each property is visible in the pixels, with 1 neutral.
|
// 28e. Each property is visible in the pixels, with 1 neutral.
|
||||||
await assetProps({ shader: ADJUST });
|
await assetProps({ shader: ADJUST });
|
||||||
await sleep(300);
|
|
||||||
const neutral = await centre();
|
const neutral = await centre();
|
||||||
check(
|
check(
|
||||||
'Adjust colors at its defaults draws exactly what no shader draws',
|
'Adjust colors at its defaults draws exactly what no shader draws',
|
||||||
@@ -285,10 +260,8 @@ export async function shader(t) {
|
|||||||
const passes = '({ shaders: __gm.shaderCount(), filters: __gm.filterCount() })';
|
const passes = '({ shaders: __gm.shaderCount(), filters: __gm.filterCount() })';
|
||||||
const passesOn = await json(passes);
|
const passesOn = await json(passes);
|
||||||
await assetProps({ shader: '' });
|
await assetProps({ shader: '' });
|
||||||
await sleep(300);
|
|
||||||
const passesOff = await json(passes);
|
const passesOff = await json(passes);
|
||||||
await assetProps({ shader: ADJUST });
|
await assetProps({ shader: ADJUST });
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'a shaded sprite draws with its own shader and no filter pass – and plain without',
|
'a shaded sprite draws with its own shader and no filter pass – and plain without',
|
||||||
passesOn.shaders === 1 && passesOn.filters === 0 && passesOff.shaders === 0 && passesOff.filters === 0,
|
passesOn.shaders === 1 && passesOn.filters === 0 && passesOff.shaders === 0 && passesOff.filters === 0,
|
||||||
@@ -298,15 +271,12 @@ export async function shader(t) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
await assetProps({ brightness: 1.25 });
|
await assetProps({ brightness: 1.25 });
|
||||||
await sleep(300);
|
|
||||||
const lighter = await centre();
|
const lighter = await centre();
|
||||||
check('brightness 1.25 adds a quarter to every channel', colourDistance(lighter, '#80c0ff') < 24, lighter);
|
check('brightness 1.25 adds a quarter to every channel', colourDistance(lighter, '#80c0ff') < 24, lighter);
|
||||||
await assetProps({ brightness: 1, contrast: 0 });
|
await assetProps({ brightness: 1, contrast: 0 });
|
||||||
await sleep(300);
|
|
||||||
const flat = await centre();
|
const flat = await centre();
|
||||||
check('contrast 0 flattens the colour to mid grey', colourDistance(flat, '#808080') < 24, flat);
|
check('contrast 0 flattens the colour to mid grey', colourDistance(flat, '#808080') < 24, flat);
|
||||||
await assetProps({ contrast: 1, saturation: 0 });
|
await assetProps({ contrast: 1, saturation: 0 });
|
||||||
await sleep(300);
|
|
||||||
const grey = await centre();
|
const grey = await centre();
|
||||||
check('saturation 0 leaves the luma as a grey', colourDistance(grey, '#7c7c7c') < 24, grey);
|
check('saturation 0 leaves the luma as a grey', colourDistance(grey, '#7c7c7c') < 24, grey);
|
||||||
await shot('28-shader-desaturated');
|
await shot('28-shader-desaturated');
|
||||||
@@ -323,11 +293,9 @@ export async function shader(t) {
|
|||||||
JSON.stringify({ at, played }),
|
JSON.stringify({ at, played }),
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 28g. Editing a shader's graph reaches every sprite wearing it: a Color wired into the new one's Output.
|
// 28g. Editing a shader's graph reaches every sprite wearing it: a Color wired into the new one's Output.
|
||||||
await assetProps({ brightness: 1, shader: made.id });
|
await assetProps({ brightness: 1, shader: made.id });
|
||||||
await sleep(300);
|
|
||||||
const through = await centre();
|
const through = await centre();
|
||||||
check(
|
check(
|
||||||
'the new shader passes the texel through',
|
'the new shader passes the texel through',
|
||||||
@@ -339,7 +307,6 @@ export async function shader(t) {
|
|||||||
const out = a.graph.nodes.find((n) => n.type === 'ShaderOutput');
|
const out = a.graph.nodes.find((n) => n.type === 'ShaderOutput');
|
||||||
a.graph.nodes.push({ id: 'sh-red', type: 'ShaderColor', x: 0, y: 200, params: { value: 0xff0000 } });
|
a.graph.nodes.push({ id: 'sh-red', type: 'ShaderColor', x: 0, y: 200, params: { value: 0xff0000 } });
|
||||||
a.graph.edges.push({ id: 'sh-red-edge', from: { node: 'sh-red', port: 'color' }, to: { node: out.id, port: 'color' } });`);
|
a.graph.edges.push({ id: 'sh-red-edge', from: { node: 'sh-red', port: 'color' }, to: { node: out.id, port: 'color' } });`);
|
||||||
await sleep(400);
|
|
||||||
const red = await centre();
|
const red = await centre();
|
||||||
check(
|
check(
|
||||||
'wiring a red Color into the Output recompiles: the sprite turns red',
|
'wiring a red Color into the Output recompiles: the sprite turns red',
|
||||||
@@ -360,7 +327,6 @@ export async function shader(t) {
|
|||||||
{ id: 'sh-fade-a', from: { node: 'sh-alpha', port: 'value' }, to: { node: 'sh-fade', port: 'a' } },
|
{ id: 'sh-fade-a', from: { node: 'sh-alpha', port: 'value' }, to: { node: 'sh-fade', port: 'a' } },
|
||||||
{ id: 'sh-fade-b', from: { node: 'sh-opacity', port: 'value' }, to: { node: 'sh-fade', port: 'b' } },
|
{ id: 'sh-fade-b', from: { node: 'sh-opacity', port: 'value' }, to: { node: 'sh-fade', port: 'b' } },
|
||||||
{ id: 'sh-fade-out', from: { node: 'sh-fade', port: 'result' }, to: { node: out.id, port: 'alpha' } });`);
|
{ id: 'sh-fade-out', from: { node: 'sh-fade', port: 'result' }, to: { node: out.id, port: 'alpha' } });`);
|
||||||
await sleep(400);
|
|
||||||
const opaque = await centre();
|
const opaque = await centre();
|
||||||
check(
|
check(
|
||||||
'an opacity property at its default 1 changes nothing',
|
'an opacity property at its default 1 changes nothing',
|
||||||
@@ -368,11 +334,9 @@ export async function shader(t) {
|
|||||||
JSON.stringify({ red, opaque, info: await info() }),
|
JSON.stringify({ red, opaque, info: await info() }),
|
||||||
);
|
);
|
||||||
await assetProps({ opacity: 0 });
|
await assetProps({ opacity: 0 });
|
||||||
await sleep(300);
|
|
||||||
const behind = await centre();
|
const behind = await centre();
|
||||||
check('opacity 0 shows what is behind the sprite', colourDistance(behind, red) > 60, { red, behind });
|
check('opacity 0 shows what is behind the sprite', colourDistance(behind, red) > 60, { red, behind });
|
||||||
await assetProps({ opacity: 0.5 });
|
await assetProps({ opacity: 0.5 });
|
||||||
await sleep(300);
|
|
||||||
const half = await centre();
|
const half = await centre();
|
||||||
const channel = (hex, i) => parseInt(hex.slice(i, i + 2), 16);
|
const channel = (hex, i) => parseInt(hex.slice(i, i + 2), 16);
|
||||||
const expected =
|
const expected =
|
||||||
@@ -391,7 +355,6 @@ export async function shader(t) {
|
|||||||
expected,
|
expected,
|
||||||
});
|
});
|
||||||
await assetProps({ opacity: 1 });
|
await assetProps({ opacity: 1 });
|
||||||
await sleep(300);
|
|
||||||
check('opacity back at 1 draws the pixel from before, byte for byte', (await centre()) === red);
|
check('opacity back at 1 draws the pixel from before, byte for byte', (await centre()) === red);
|
||||||
|
|
||||||
// 28i. A Number node is a constant in the fragment: red times 0.5 is half red; typed back to 1,
|
// 28i. A Number node is a constant in the fragment: red times 0.5 is half red; typed back to 1,
|
||||||
@@ -407,25 +370,22 @@ export async function shader(t) {
|
|||||||
{ id: 'sh-dim-a', from: { node: 'sh-red', port: 'color' }, to: { node: 'sh-dim', port: 'a' } },
|
{ id: 'sh-dim-a', from: { node: 'sh-red', port: 'color' }, to: { node: 'sh-dim', port: 'a' } },
|
||||||
{ id: 'sh-dim-b', from: { node: 'sh-half', port: 'value' }, to: { node: 'sh-dim', port: 'b' } },
|
{ id: 'sh-dim-b', from: { node: 'sh-half', port: 'value' }, to: { node: 'sh-dim', port: 'b' } },
|
||||||
{ id: 'sh-dim-out', from: { node: 'sh-dim', port: 'result' }, to: { node: out.id, port: 'color' } });`);
|
{ id: 'sh-dim-out', from: { node: 'sh-dim', port: 'result' }, to: { node: out.id, port: 'color' } });`);
|
||||||
await sleep(400);
|
|
||||||
const dimmed = await centre();
|
const dimmed = await centre();
|
||||||
check('a Number of 0.5 into a Multiply halves the red', colourDistance(dimmed, '#800000') < 6, {
|
check('a Number of 0.5 into a Multiply halves the red', colourDistance(dimmed, '#800000') < 6, {
|
||||||
red,
|
red,
|
||||||
dimmed,
|
dimmed,
|
||||||
});
|
});
|
||||||
await evaluate("__gm.showTab('shader')");
|
await showTab('shader');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the Number node draws its value as a setting row',
|
'the Number node draws its value as a setting row',
|
||||||
(await json("__gm.shader.nodeSettings('sh-half')"))?.join() === 'Value: 0.500',
|
(await json("__gm.shader.nodeSettings('sh-half')"))?.join() === 'Value: 0.500',
|
||||||
JSON.stringify(await json("__gm.shader.nodeSettings('sh-half')")),
|
JSON.stringify(await json("__gm.shader.nodeSettings('sh-half')")),
|
||||||
);
|
);
|
||||||
await shot('28-number-node');
|
await shot('28-number-node');
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||||
a.graph.nodes.find((n) => n.id === 'sh-half').params.value = 1`);
|
a.graph.nodes.find((n) => n.id === 'sh-half').params.value = 1`);
|
||||||
await sleep(400);
|
|
||||||
check('the Number typed to 1 gives the red from before, byte for byte', (await centre()) === red, {
|
check('the Number typed to 1 gives the red from before, byte for byte', (await centre()) === red, {
|
||||||
red,
|
red,
|
||||||
now: await centre(),
|
now: await centre(),
|
||||||
@@ -468,7 +428,6 @@ export async function shader(t) {
|
|||||||
[edge('uv', port, 'cut', 'value'), edge('cut', 'result', 'mix', 't')],
|
[edge('uv', port, 'cut', 'value'), edge('cut', 'result', 'mix', 't')],
|
||||||
);
|
);
|
||||||
await cutAt('u');
|
await cutAt('u');
|
||||||
await sleep(400);
|
|
||||||
const uLeft = await pixel(-0.3, -0.3);
|
const uLeft = await pixel(-0.3, -0.3);
|
||||||
const uRight = await pixel(0.3, -0.3);
|
const uRight = await pixel(0.3, -0.3);
|
||||||
check(
|
check(
|
||||||
@@ -477,7 +436,6 @@ export async function shader(t) {
|
|||||||
{ uLeft, uRight },
|
{ uLeft, uRight },
|
||||||
);
|
);
|
||||||
await cutAt('v');
|
await cutAt('v');
|
||||||
await sleep(400);
|
|
||||||
const vTop = await pixel(-0.3, -0.3);
|
const vTop = await pixel(-0.3, -0.3);
|
||||||
const vBottom = await pixel(-0.3, 0.3);
|
const vBottom = await pixel(-0.3, 0.3);
|
||||||
check(
|
check(
|
||||||
@@ -501,7 +459,6 @@ export async function shader(t) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
await cutAfterPow(2);
|
await cutAfterPow(2);
|
||||||
await sleep(400);
|
|
||||||
const squaredNear = await pixel(0.1, -0.3);
|
const squaredNear = await pixel(0.1, -0.3);
|
||||||
const squaredFar = await pixel(0.3, -0.3);
|
const squaredFar = await pixel(0.3, -0.3);
|
||||||
check(
|
check(
|
||||||
@@ -510,7 +467,6 @@ export async function shader(t) {
|
|||||||
{ squaredNear, squaredFar },
|
{ squaredNear, squaredFar },
|
||||||
);
|
);
|
||||||
await cutAfterPow(0.5);
|
await cutAfterPow(0.5);
|
||||||
await sleep(400);
|
|
||||||
const rootNear = await pixel(-0.3, -0.3);
|
const rootNear = await pixel(-0.3, -0.3);
|
||||||
const rootFar = await pixel(-0.15, -0.3);
|
const rootFar = await pixel(-0.15, -0.3);
|
||||||
check(
|
check(
|
||||||
@@ -534,7 +490,6 @@ export async function shader(t) {
|
|||||||
edge('cut', 'result', 'mix', 't'),
|
edge('cut', 'result', 'mix', 't'),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
await sleep(400);
|
|
||||||
const foldLeft = await pixel(-0.4, -0.3);
|
const foldLeft = await pixel(-0.4, -0.3);
|
||||||
const foldMiddle = await pixel(0, -0.3);
|
const foldMiddle = await pixel(0, -0.3);
|
||||||
const foldRight = await pixel(0.4, -0.3);
|
const foldRight = await pixel(0.4, -0.3);
|
||||||
@@ -564,7 +519,6 @@ export async function shader(t) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
await namedProperty('width');
|
await namedProperty('width');
|
||||||
await sleep(400);
|
|
||||||
const spriteProp = await info();
|
const spriteProp = await info();
|
||||||
check(
|
check(
|
||||||
'a property named like a sprite row is a problem on the node, not a second width row (#63)',
|
'a property named like a sprite row is a problem on the node, not a second width row (#63)',
|
||||||
@@ -573,7 +527,6 @@ export async function shader(t) {
|
|||||||
JSON.stringify({ spriteProp, problem: await json("__gm.shader.nodeProblem('named')") }),
|
JSON.stringify({ spriteProp, problem: await json("__gm.shader.nodeProblem('named')") }),
|
||||||
);
|
);
|
||||||
await namedProperty('amount');
|
await namedProperty('amount');
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'the same graph under a free name compiles: a quarter of the red',
|
'the same graph under a free name compiles: a quarter of the red',
|
||||||
(await info()) === '1 property' && colourDistance(await pixel(-0.3, -0.3), '#400000') < 6,
|
(await info()) === '1 property' && colourDistance(await pixel(-0.3, -0.3), '#400000') < 6,
|
||||||
@@ -598,10 +551,8 @@ export async function shader(t) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
await dividedBy(2);
|
await dividedBy(2);
|
||||||
await sleep(400);
|
|
||||||
const halved = await pixel(-0.3, -0.3);
|
const halved = await pixel(-0.3, -0.3);
|
||||||
await dividedBy(0);
|
await dividedBy(0);
|
||||||
await sleep(400);
|
|
||||||
const byZero = await pixel(-0.3, -0.3);
|
const byZero = await pixel(-0.3, -0.3);
|
||||||
check(
|
check(
|
||||||
'Divide by 2 halves the red, and dividing by 0 is 0 – a black pixel, not the driver’s choice (#64)',
|
'Divide by 2 halves the red, and dividing by 0 is 0 – a black pixel, not the driver’s choice (#64)',
|
||||||
@@ -618,7 +569,6 @@ export async function shader(t) {
|
|||||||
],
|
],
|
||||||
[edge('red', 'color', 'hold', 'value'), edge('hold', 'result', 'out', 'color')],
|
[edge('red', 'color', 'hold', 'value'), edge('hold', 'result', 'out', 'color')],
|
||||||
);
|
);
|
||||||
await sleep(400);
|
|
||||||
const held = await pixel(-0.3, -0.3);
|
const held = await pixel(-0.3, -0.3);
|
||||||
check('Clamp at 0..0.25 holds full red down to a quarter', colourDistance(held, '#400000') < 6, held);
|
check('Clamp at 0..0.25 holds full red down to a quarter', colourDistance(held, '#400000') < 6, held);
|
||||||
|
|
||||||
@@ -661,7 +611,6 @@ export async function shader(t) {
|
|||||||
[node('flip', 'ShaderSubtract', { a: 1 }, 260, 0)],
|
[node('flip', 'ShaderSubtract', { a: 1 }, 260, 0)],
|
||||||
[edge('uv', 'u', 'flip', 'b'), edge('flip', 'result', 'sample', 'u')],
|
[edge('uv', 'u', 'flip', 'b'), edge('flip', 'result', 'sample', 'u')],
|
||||||
);
|
);
|
||||||
await sleep(400);
|
|
||||||
const flipLeft = await pixel(-0.3, -0.3);
|
const flipLeft = await pixel(-0.3, -0.3);
|
||||||
const flipRight = await pixel(0.3, -0.3);
|
const flipRight = await pixel(0.3, -0.3);
|
||||||
check(
|
check(
|
||||||
@@ -675,7 +624,6 @@ export async function shader(t) {
|
|||||||
[edge('uv', 'u', 'shift', 'a'), edge('shift', 'result', 'sample', 'u')],
|
[edge('uv', 'u', 'shift', 'a'), edge('shift', 'result', 'sample', 'u')],
|
||||||
{ edge: edgeMode },
|
{ edge: edgeMode },
|
||||||
);
|
);
|
||||||
await sleep(400);
|
|
||||||
return { left: await pixel(-0.3, -0.3), right: await pixel(0.3, -0.3) };
|
return { left: await pixel(-0.3, -0.3), right: await pixel(0.3, -0.3) };
|
||||||
};
|
};
|
||||||
const repeat = await scrolled('repeat');
|
const repeat = await scrolled('repeat');
|
||||||
@@ -698,7 +646,6 @@ export async function shader(t) {
|
|||||||
[edge('uv', 'u', 'far', 'a'), edge('far', 'result', 'sample', 'u')],
|
[edge('uv', 'u', 'far', 'a'), edge('far', 'result', 'sample', 'u')],
|
||||||
outParams,
|
outParams,
|
||||||
);
|
);
|
||||||
await sleep(400);
|
|
||||||
return pixel(0.3, -0.3);
|
return pixel(0.3, -0.3);
|
||||||
};
|
};
|
||||||
const byDefault = await scrolledFar({});
|
const byDefault = await scrolledFar({});
|
||||||
@@ -708,17 +655,14 @@ export async function shader(t) {
|
|||||||
colourDistance(byDefault, RIGHT) < 12 && colourDistance(byMirror, LEFT) < 12,
|
colourDistance(byDefault, RIGHT) < 12 && colourDistance(byMirror, LEFT) < 12,
|
||||||
{ byDefault, byMirror },
|
{ byDefault, byMirror },
|
||||||
);
|
);
|
||||||
await evaluate("__gm.showTab('shader')");
|
await showTab('shader');
|
||||||
await sleep(300);
|
|
||||||
await shot('28-shader-scroll-graph');
|
await shot('28-shader-scroll-graph');
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
const sizeAtLeast = async (px) => {
|
const sizeAtLeast = async (px) => {
|
||||||
await redOrBlue(
|
await redOrBlue(
|
||||||
[node('size', 'ShaderTextureSize'), node('cut', 'ShaderStep', { edge: px }, 260, 0)],
|
[node('size', 'ShaderTextureSize'), node('cut', 'ShaderStep', { edge: px }, 260, 0)],
|
||||||
[edge('size', 'width', 'cut', 'value'), edge('cut', 'result', 'mix', 't')],
|
[edge('size', 'width', 'cut', 'value'), edge('cut', 'result', 'mix', 't')],
|
||||||
);
|
);
|
||||||
await sleep(400);
|
|
||||||
return pixel(-0.3, -0.3);
|
return pixel(-0.3, -0.3);
|
||||||
};
|
};
|
||||||
const wide = await sizeAtLeast(31.5);
|
const wide = await sizeAtLeast(31.5);
|
||||||
@@ -788,21 +732,17 @@ export async function shader(t) {
|
|||||||
await sleep(500);
|
await sleep(500);
|
||||||
const playing = await json('__gm.animates()');
|
const playing = await json('__gm.animates()');
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
check('and so does the game view while playing', playing.game === true, JSON.stringify(playing));
|
check('and so does the game view while playing', playing.game === true, JSON.stringify(playing));
|
||||||
await assetProps({ shader: '' });
|
await assetProps({ shader: '' });
|
||||||
await sleep(400);
|
|
||||||
check(
|
check(
|
||||||
'with the shader off the scene view stops redrawing',
|
'with the shader off the scene view stops redrawing',
|
||||||
(await json('__gm.animates()')).scene === false,
|
(await json('__gm.animates()')).scene === false,
|
||||||
JSON.stringify(await json('__gm.animates()')),
|
JSON.stringify(await json('__gm.animates()')),
|
||||||
);
|
);
|
||||||
await assetProps({ texture: '', color: 0x4080ff, shader: made.id });
|
await assetProps({ texture: '', color: 0x4080ff, shader: made.id });
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 28j. Back to None: no filter at all, the plain colour returns bit for bit.
|
// 28j. Back to None: no filter at all, the plain colour returns bit for bit.
|
||||||
await assetProps({ shader: '' });
|
await assetProps({ shader: '' });
|
||||||
await sleep(300);
|
|
||||||
const back = await centre();
|
const back = await centre();
|
||||||
check(
|
check(
|
||||||
'switching back to None restores the plain colour',
|
'switching back to None restores the plain colour',
|
||||||
@@ -817,7 +757,6 @@ export async function shader(t) {
|
|||||||
const scene = __gm.state.activeScene;
|
const scene = __gm.state.activeScene;
|
||||||
scene.objects = scene.objects.filter((o) => o.id !== 'oshade')`);
|
scene.objects = scene.objects.filter((o) => o.id !== 'oshade')`);
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing behind but the fixture’s shader',
|
'the step leaves nothing behind but the fixture’s shader',
|
||||||
(await json("__gm.state.project.assets.some((a) => a.id === 'shade-sprite')")) === false &&
|
(await json("__gm.state.project.assets.some((a) => a.id === 'shade-sprite')")) === false &&
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export async function transform(t) {
|
|||||||
entity,
|
entity,
|
||||||
stop,
|
stop,
|
||||||
shot,
|
shot,
|
||||||
|
showTab,
|
||||||
} = t;
|
} = t;
|
||||||
const field = (label) => json(`__gm.inspectorField(${JSON.stringify(label)})`);
|
const field = (label) => json(`__gm.inspectorField(${JSON.stringify(label)})`);
|
||||||
const rect = (id) => json(`__gm.objectRect(${JSON.stringify(id)})`);
|
const rect = (id) => json(`__gm.objectRect(${JSON.stringify(id)})`);
|
||||||
@@ -47,7 +48,6 @@ export async function transform(t) {
|
|||||||
);
|
);
|
||||||
const selectObject = async (id) => {
|
const selectObject = async (id) => {
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [id], objectId: id });
|
await select({ kind: 'object', holderId: 'scene1', objectIds: [id], objectId: id });
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
const remove = (ids) =>
|
const remove = (ids) =>
|
||||||
mutate(`
|
mutate(`
|
||||||
@@ -57,8 +57,7 @@ export async function transform(t) {
|
|||||||
|
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 29a. A plain red 100×40 sprite in its lane, and a second camera to read the inspector off;
|
// 29a. A plain red 100×40 sprite in its lane, and a second camera to read the inspector off;
|
||||||
// a sprite object has the transform rows, a camera object has not.
|
// a sprite object has the transform rows, a camera object has not.
|
||||||
@@ -118,7 +117,6 @@ export async function transform(t) {
|
|||||||
|
|
||||||
// 29c. Rotation 90: the box stands up – its frame swaps sides, the pixels move with it.
|
// 29c. Rotation 90: the box stands up – its frame swaps sides, the pixels move with it.
|
||||||
await override(BOX, { rotation: 90 });
|
await override(BOX, { rotation: 90 });
|
||||||
await sleep(300);
|
|
||||||
const turned = await rect(BOX);
|
const turned = await rect(BOX);
|
||||||
const turnedPx = { right: await px(40, 0), below: await px(0, 40) };
|
const turnedPx = { right: await px(40, 0), below: await px(0, 40) };
|
||||||
check(
|
check(
|
||||||
@@ -132,7 +130,6 @@ export async function transform(t) {
|
|||||||
|
|
||||||
// 29d. Scale 2 on top: twice the frame, red where the upright box would have ended long ago.
|
// 29d. Scale 2 on top: twice the frame, red where the upright box would have ended long ago.
|
||||||
await override(BOX, { scale: 2 });
|
await override(BOX, { scale: 2 });
|
||||||
await sleep(300);
|
|
||||||
const grown = await rect(BOX);
|
const grown = await rect(BOX);
|
||||||
const far = await px(0, 90);
|
const far = await px(0, 90);
|
||||||
check(
|
check(
|
||||||
@@ -159,7 +156,6 @@ export async function transform(t) {
|
|||||||
JSON.stringify(plain),
|
JSON.stringify(plain),
|
||||||
);
|
);
|
||||||
await override(BOX, { mirrorX: true });
|
await override(BOX, { mirrorX: true });
|
||||||
await sleep(300);
|
|
||||||
const mirrored = { left: await px(-30, 0), right: await px(30, 0) };
|
const mirrored = { left: await px(-30, 0), right: await px(30, 0) };
|
||||||
check(
|
check(
|
||||||
'Mirror X swaps the halves: green left, red right',
|
'Mirror X swaps the halves: green left, red right',
|
||||||
@@ -167,7 +163,6 @@ export async function transform(t) {
|
|||||||
JSON.stringify(mirrored),
|
JSON.stringify(mirrored),
|
||||||
);
|
);
|
||||||
await override(BOX, { mirrorY: true });
|
await override(BOX, { mirrorY: true });
|
||||||
await sleep(300);
|
|
||||||
const both = { left: await px(-30, 0), right: await px(30, 0) };
|
const both = { left: await px(-30, 0), right: await px(30, 0) };
|
||||||
check(
|
check(
|
||||||
'Mirror Y on top leaves the halves where Mirror X put them',
|
'Mirror Y on top leaves the halves where Mirror X put them',
|
||||||
@@ -176,17 +171,14 @@ export async function transform(t) {
|
|||||||
);
|
);
|
||||||
await override(BOX, { mirrorX: false, mirrorY: false });
|
await override(BOX, { mirrorX: false, mirrorY: false });
|
||||||
await spriteProps({ texture: '', color: 0xff0000 });
|
await spriteProps({ texture: '', color: 0xff0000 });
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 29f. A child 100 to the right of its parent: once the parent turns a quarter, the child hangs
|
// 29f. A child 100 to the right of its parent: once the parent turns a quarter, the child hangs
|
||||||
// 100 below it – and is drawn turned itself.
|
// 100 below it – and is drawn turned itself.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const scene = __gm.state.activeScene;
|
const scene = __gm.state.activeScene;
|
||||||
scene.objects.push({ id: ${JSON.stringify(CHILD)}, assetId: ${JSON.stringify(SPRITE)}, name: '', position: { x: 100, y: 0, z: 1 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: ${JSON.stringify(BOX)}, instanceEdits: {} });`);
|
scene.objects.push({ id: ${JSON.stringify(CHILD)}, assetId: ${JSON.stringify(SPRITE)}, name: '', position: { x: 100, y: 0, z: 1 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: ${JSON.stringify(BOX)}, instanceEdits: {} });`);
|
||||||
await sleep(300);
|
|
||||||
const flat = { parent: await screen(BOX), child: await screen(CHILD) };
|
const flat = { parent: await screen(BOX), child: await screen(CHILD) };
|
||||||
await override(BOX, { rotation: 90 });
|
await override(BOX, { rotation: 90 });
|
||||||
await sleep(300);
|
|
||||||
const hung = { parent: await screen(BOX), child: await screen(CHILD), childRect: await rect(CHILD) };
|
const hung = { parent: await screen(BOX), child: await screen(CHILD), childRect: await rect(CHILD) };
|
||||||
check(
|
check(
|
||||||
'a child follows its parent’s turn: 100 to the right becomes 100 below, drawn turned',
|
'a child follows its parent’s turn: 100 to the right becomes 100 below, drawn turned',
|
||||||
@@ -202,7 +194,6 @@ export async function transform(t) {
|
|||||||
// 29g. Dragging the child 30 screen pixels down moves it along the parent's x axis – which now
|
// 29g. Dragging the child 30 screen pixels down moves it along the parent's x axis – which now
|
||||||
// points down – so its local x grows by 30 / scale and y stays.
|
// points down – so its local x grows by 30 / scale and y stays.
|
||||||
await drag(hung.child.x, hung.child.y, hung.child.x, hung.child.y + 30, { steps: 8 });
|
await drag(hung.child.x, hung.child.y, hung.child.x, hung.child.y + 30, { steps: 8 });
|
||||||
await sleep(300);
|
|
||||||
const dragged = await object(CHILD);
|
const dragged = await object(CHILD);
|
||||||
check(
|
check(
|
||||||
'a drag under a turned parent lands in the parent’s frame: local x + 30 / scale, y unchanged',
|
'a drag under a turned parent lands in the parent’s frame: local x + 30 / scale, y unchanged',
|
||||||
@@ -210,7 +201,6 @@ export async function transform(t) {
|
|||||||
JSON.stringify({ position: dragged.position, expected: 100 + 30 / s }),
|
JSON.stringify({ position: dragged.position, expected: 100 + 30 / s }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(200);
|
|
||||||
check('undo puts the child back', (await object(CHILD)).position.x === 100);
|
check('undo puts the child back', (await object(CHILD)).position.x === 100);
|
||||||
await selectObject(CHILD);
|
await selectObject(CHILD);
|
||||||
const gizmos = await json('__gm.gizmos()');
|
const gizmos = await json('__gm.gizmos()');
|
||||||
@@ -236,7 +226,6 @@ export async function transform(t) {
|
|||||||
);
|
);
|
||||||
__gm.state.select(null)`);
|
__gm.state.select(null)`);
|
||||||
await press('Enter', { ctrl: true });
|
await press('Enter', { ctrl: true });
|
||||||
await sleep(300);
|
|
||||||
await holdKey('ArrowRight', 1000);
|
await holdKey('ArrowRight', 1000);
|
||||||
const body = await entity('otf-body');
|
const body = await entity('otf-body');
|
||||||
await stop();
|
await stop();
|
||||||
@@ -255,7 +244,6 @@ export async function transform(t) {
|
|||||||
project.assets = project.assets.filter((a) => !gone.has(a.id));
|
project.assets = project.assets.filter((a) => !gone.has(a.id));
|
||||||
__gm.lookAt(0, 0)`);
|
__gm.lookAt(0, 0)`);
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing behind',
|
'the step leaves nothing behind',
|
||||||
(await json(`__gm.state.project.assets.some((a) => a.id === ${JSON.stringify(SPRITE)})`)) === false &&
|
(await json(`__gm.state.project.assets.some((a) => a.id === ${JSON.stringify(SPRITE)})`)) === false &&
|
||||||
|
|||||||
+3
-14
@@ -17,7 +17,8 @@ const SERIF_BYTES = 380660;
|
|||||||
const MONO_BYTES = 343140;
|
const MONO_BYTES = 343140;
|
||||||
|
|
||||||
export async function font(t) {
|
export async function font(t) {
|
||||||
const { check, sleep, json, evaluate, mutate, click, clickText, locate, press, play, stop, shot } = t;
|
const { check, sleep, json, evaluate, mutate, click, clickText, locate, press, play, stop, shot, showTab } =
|
||||||
|
t;
|
||||||
const { select } = t;
|
const { select } = t;
|
||||||
const assetsOfType = (type) =>
|
const assetsOfType = (type) =>
|
||||||
json(`__gm.state.project.assets.filter((a) => a.type === ${JSON.stringify(type)})
|
json(`__gm.state.project.assets.filter((a) => a.type === ${JSON.stringify(type)})
|
||||||
@@ -27,7 +28,6 @@ export async function font(t) {
|
|||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
/** A face arrives over the network, so what it changes is awaited rather than slept for. */
|
/** A face arrives over the network, so what it changes is awaited rather than slept for. */
|
||||||
const waitFor = async (read, done, tries = 20) => {
|
const waitFor = async (read, done, tries = 20) => {
|
||||||
@@ -42,8 +42,7 @@ export async function font(t) {
|
|||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 30a. What the server holds, straight from the API: the two files the seed put there.
|
// 30a. What the server holds, straight from the API: the two files the seed put there.
|
||||||
// `evaluate`, not `json`: the latter stringifies the promise itself rather than what it yields.
|
// `evaluate`, not `json`: the latter stringifies the promise itself rather than what it yields.
|
||||||
@@ -66,7 +65,6 @@ export async function font(t) {
|
|||||||
// 30b. "+ → Font" offers them; a font is picked rather than created empty, like a texture.
|
// 30b. "+ → Font" offers them; a font is picked rather than created empty, like a texture.
|
||||||
await evaluate('__gm.console.clear()');
|
await evaluate('__gm.console.clear()');
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await sleep(200);
|
|
||||||
await pickAssetType(t, 'Font');
|
await pickAssetType(t, 'Font');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const picker = await json('__gm.fontPicker()');
|
const picker = await json('__gm.fontPicker()');
|
||||||
@@ -93,9 +91,7 @@ export async function font(t) {
|
|||||||
);
|
);
|
||||||
const row = await json(`__gm.fontPickerRow(${JSON.stringify(serif.file)})`);
|
const row = await json(`__gm.fontPickerRow(${JSON.stringify(serif.file)})`);
|
||||||
await click(row.x, row.y);
|
await click(row.x, row.y);
|
||||||
await sleep(200);
|
|
||||||
await clickText('Use font');
|
await clickText('Use font');
|
||||||
await sleep(400);
|
|
||||||
const fonts = await assetsOfType('font');
|
const fonts = await assetsOfType('font');
|
||||||
check(
|
check(
|
||||||
'picking a row makes a font asset of that file, named after the file rather than "Font"',
|
'picking a row makes a font asset of that file, named after the file rather than "Font"',
|
||||||
@@ -118,7 +114,6 @@ export async function font(t) {
|
|||||||
name: '', position: { x: 0, y: ${LANE}, z: 0 }, editorOffset: { x: 0, y: 0 },
|
name: '', position: { x: 0, y: ${LANE}, z: 0 }, editorOffset: { x: 0, y: 0 },
|
||||||
overrides: {}, parentId: '', instanceEdits: {} });
|
overrides: {}, parentId: '', instanceEdits: {} });
|
||||||
__gm.lookAt(0, ${LANE});`);
|
__gm.lookAt(0, ${LANE});`);
|
||||||
await sleep(400);
|
|
||||||
const plain = await info();
|
const plain = await info();
|
||||||
check(
|
check(
|
||||||
'a text wearing no font is drawn in the editor’s own typeface',
|
'a text wearing no font is drawn in the editor’s own typeface',
|
||||||
@@ -129,7 +124,6 @@ export async function font(t) {
|
|||||||
|
|
||||||
// 30d. Wearing the font, chosen the way a user chooses it: the Font row of the text object.
|
// 30d. Wearing the font, chosen the way a user chooses it: the Font row of the text object.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [TEXT_OBJECT], objectId: TEXT_OBJECT });
|
await select({ kind: 'object', holderId: 'scene1', objectIds: [TEXT_OBJECT], objectId: TEXT_OBJECT });
|
||||||
await sleep(300);
|
|
||||||
// The label is no anchor on its own: the assets panel has a row per font. The dropdown of the
|
// The label is no anchor on its own: the assets panel has a row per font. The dropdown of the
|
||||||
// Font row is the last "None" in the window – Parent says it above, nothing says it below.
|
// Font row is the last "None" in the window – Parent says it above, nothing says it below.
|
||||||
const fontRow = await json("__gm.inspectorField('Font', true)");
|
const fontRow = await json("__gm.inspectorField('Font', true)");
|
||||||
@@ -140,9 +134,7 @@ export async function font(t) {
|
|||||||
JSON.stringify({ fontRow, none }),
|
JSON.stringify({ fontRow, none }),
|
||||||
);
|
);
|
||||||
await click(fontRow.x, fontRow.y);
|
await click(fontRow.x, fontRow.y);
|
||||||
await sleep(250);
|
|
||||||
await clickLast('Bookish (font)');
|
await clickLast('Bookish (font)');
|
||||||
await sleep(400);
|
|
||||||
const chosen = await json(
|
const chosen = await json(
|
||||||
`__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(TEXT_OBJECT)}).overrides.font`,
|
`__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(TEXT_OBJECT)}).overrides.font`,
|
||||||
);
|
);
|
||||||
@@ -155,7 +147,6 @@ export async function font(t) {
|
|||||||
await shot('30-font-scene');
|
await shot('30-font-scene');
|
||||||
// The asset's own inspector is where a font is looked at; it has nothing else to show.
|
// The asset's own inspector is where a font is looked at; it has nothing else to show.
|
||||||
await select({ kind: 'asset', assetId: fontId });
|
await select({ kind: 'asset', assetId: fontId });
|
||||||
await sleep(300);
|
|
||||||
await shot('30-font-asset');
|
await shot('30-font-asset');
|
||||||
|
|
||||||
// 30e. The game draws through the same path, so the face is in play as well. The camera frames
|
// 30e. The game draws through the same path, so the face is in play as well. The camera frames
|
||||||
@@ -177,7 +168,6 @@ export async function font(t) {
|
|||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await place(`{ x: 0, y: ${LANE} }`);
|
await place(`{ x: 0, y: ${LANE} }`);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 30f. An id that no longer resolves is no font at all – the text falls back rather than vanishing.
|
// 30f. An id that no longer resolves is no font at all – the text falls back rather than vanishing.
|
||||||
await mutate(
|
await mutate(
|
||||||
@@ -223,7 +213,6 @@ export async function font(t) {
|
|||||||
(o) => o.id !== ${JSON.stringify(TEXT_OBJECT)});
|
(o) => o.id !== ${JSON.stringify(TEXT_OBJECT)});
|
||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
__gm.lookAt(0, 0);`);
|
__gm.lookAt(0, 0);`);
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing of its own behind',
|
'the step leaves nothing of its own behind',
|
||||||
(await assetsOfType('font')).length === 0 && (await info()) === null,
|
(await assetsOfType('font')).length === 0 && (await info()) === null,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ const SOURCE = 'pt-source';
|
|||||||
const RATE = 20;
|
const RATE = 20;
|
||||||
|
|
||||||
export async function particles(t) {
|
export async function particles(t) {
|
||||||
const { check, sleep, json, evaluate, mutate, click, clickText, locate, typeText, press, shot } = t;
|
const { check, sleep, json, evaluate, mutate, click, clickText, locate, typeText, press, shot, showTab } =
|
||||||
|
t;
|
||||||
const { drag } = t;
|
const { drag } = t;
|
||||||
const { select } = t;
|
const { select } = t;
|
||||||
/** Freeze, restart every source's clock, then step the clock to `seconds` and let it redraw. */
|
/** Freeze, restart every source's clock, then step the clock to `seconds` and let it redraw. */
|
||||||
@@ -33,21 +34,16 @@ export async function particles(t) {
|
|||||||
const drawn = (limit = 8) => json(`__gm.particlesOf(${JSON.stringify(SOURCE)}, 'scene', ${limit})`);
|
const drawn = (limit = 8) => json(`__gm.particlesOf(${JSON.stringify(SOURCE)}, 'scene', ${limit})`);
|
||||||
const selectSource = async () => {
|
const selectSource = async () => {
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [SOURCE], objectId: SOURCE });
|
await select({ kind: 'object', holderId: 'scene1', objectIds: [SOURCE], objectId: SOURCE });
|
||||||
await sleep(350);
|
|
||||||
};
|
};
|
||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
/** Places a node from the palette. The list scrolls, so the name is searched into view first. */
|
/** Places a node from the palette. The list scrolls, so the name is searched into view first. */
|
||||||
const placeNodeAt = async (label, point) => {
|
const placeNodeAt = async (label, point) => {
|
||||||
await click(point.x, point.y, 'right');
|
await click(point.x, point.y, 'right');
|
||||||
await sleep(300);
|
|
||||||
await typeText(label);
|
await typeText(label);
|
||||||
await sleep(200);
|
|
||||||
await clickLast(label);
|
await clickLast(label);
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
const placeNode = async (label, dx, dy) => {
|
const placeNode = async (label, dx, dy) => {
|
||||||
const canvas = await json('__gm.particles.canvasRect()');
|
const canvas = await json('__gm.particles.canvasRect()');
|
||||||
@@ -66,29 +62,24 @@ export async function particles(t) {
|
|||||||
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).graph.nodes.push({
|
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).graph.nodes.push({
|
||||||
id: ${JSON.stringify(id)}, type: ${JSON.stringify(type)}, x: 0, y: 0,
|
id: ${JSON.stringify(id)}, type: ${JSON.stringify(type)}, x: 0, y: 0,
|
||||||
params: ${JSON.stringify(params)} });`);
|
params: ${JSON.stringify(params)} });`);
|
||||||
await sleep(150);
|
|
||||||
};
|
};
|
||||||
const link = async (id, from, fromPort, to, toPort) => {
|
const link = async (id, from, fromPort, to, toPort) => {
|
||||||
await mutate(`
|
await mutate(`
|
||||||
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).graph.edges.push({
|
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).graph.edges.push({
|
||||||
id: ${JSON.stringify(id)}, from: { node: ${JSON.stringify(from)}, port: ${JSON.stringify(fromPort)} },
|
id: ${JSON.stringify(id)}, from: { node: ${JSON.stringify(from)}, port: ${JSON.stringify(fromPort)} },
|
||||||
to: { node: ${JSON.stringify(to)}, port: ${JSON.stringify(toPort)} } });`);
|
to: { node: ${JSON.stringify(to)}, port: ${JSON.stringify(toPort)} } });`);
|
||||||
await sleep(150);
|
|
||||||
};
|
};
|
||||||
let assetId = '';
|
let assetId = '';
|
||||||
|
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 31a. "+ → Particles" makes the asset and opens its tab; with no Output there is nothing to run.
|
// 31a. "+ → Particles" makes the asset and opens its tab; with no Output there is nothing to run.
|
||||||
await evaluate('__gm.console.clear()');
|
await evaluate('__gm.console.clear()');
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await sleep(200);
|
|
||||||
const picked = await pickAssetType(t, 'Particles');
|
const picked = await pickAssetType(t, 'Particles');
|
||||||
await sleep(400);
|
|
||||||
const created = await json(`{
|
const created = await json(`{
|
||||||
tab: __gm.tab(),
|
tab: __gm.tab(),
|
||||||
current: __gm.particles.current(),
|
current: __gm.particles.current(),
|
||||||
@@ -107,12 +98,10 @@ export async function particles(t) {
|
|||||||
assetId = created.assets[0];
|
assetId = created.assets[0];
|
||||||
// Renamed away from "Particles": the tab button says that too, and a lookup by text would find it.
|
// Renamed away from "Particles": the tab button says that too, and a lookup by text would find it.
|
||||||
await mutate(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).name = 'Sparks'`);
|
await mutate(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).name = 'Sparks'`);
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 31b. The palette of this tab holds particle nodes and nothing else.
|
// 31b. The palette of this tab holds particle nodes and nothing else.
|
||||||
const canvas = await json('__gm.particles.canvasRect()');
|
const canvas = await json('__gm.particles.canvasRect()');
|
||||||
await click(canvas.x + 80, canvas.y + 60, 'right');
|
await click(canvas.x + 80, canvas.y + 60, 'right');
|
||||||
await sleep(300);
|
|
||||||
const palette = await json('__gm.overlayTexts()');
|
const palette = await json('__gm.overlayTexts()');
|
||||||
check(
|
check(
|
||||||
'the palette offers particle nodes, not behavior ones',
|
'the palette offers particle nodes, not behavior ones',
|
||||||
@@ -120,7 +109,6 @@ export async function particles(t) {
|
|||||||
JSON.stringify(palette.slice(0, 12)),
|
JSON.stringify(palette.slice(0, 12)),
|
||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 31c. One Output node is a whole graph: a particle without rules is one at the source, full size.
|
// 31c. One Output node is a whole graph: a particle without rules is one at the source, full size.
|
||||||
await placeNode('Output', 80, 60);
|
await placeNode('Output', 80, 60);
|
||||||
@@ -148,7 +136,6 @@ export async function particles(t) {
|
|||||||
// the button has to offer the way back, and Restart on a held panel stands at second 0.
|
// the button has to offer the way back, and Restart on a held panel stands at second 0.
|
||||||
const track = await json('__gm.particles.sliderAt()');
|
const track = await json('__gm.particles.sliderAt()');
|
||||||
await click(track.x, track.y);
|
await click(track.x, track.y);
|
||||||
await sleep(200);
|
|
||||||
const dragged = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
const dragged = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||||
check(
|
check(
|
||||||
'dragging the time slider holds the panel, and the button then offers to run it again',
|
'dragging the time slider holds the panel, and the button then offers to run it again',
|
||||||
@@ -160,7 +147,6 @@ export async function particles(t) {
|
|||||||
JSON.stringify(dragged),
|
JSON.stringify(dragged),
|
||||||
);
|
);
|
||||||
await clickText('Restart');
|
await clickText('Restart');
|
||||||
await sleep(200);
|
|
||||||
const restarted = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
const restarted = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||||
check(
|
check(
|
||||||
'Restart puts a held panel back to second 0, and the readout and slider with it',
|
'Restart puts a held panel back to second 0, and the readout and slider with it',
|
||||||
@@ -172,7 +158,6 @@ export async function particles(t) {
|
|||||||
JSON.stringify(restarted),
|
JSON.stringify(restarted),
|
||||||
);
|
);
|
||||||
await clickText('Run');
|
await clickText('Run');
|
||||||
await sleep(250);
|
|
||||||
const running = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
const running = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||||
check(
|
check(
|
||||||
'the same button lets it run again and says "Hold" once it does',
|
'the same button lets it run again and says "Hold" once it does',
|
||||||
@@ -196,7 +181,6 @@ export async function particles(t) {
|
|||||||
JSON.stringify(beside),
|
JSON.stringify(beside),
|
||||||
);
|
);
|
||||||
await drag(beside.split.x, beside.split.y, beside.split.x - 80, beside.split.y, { steps: 8 });
|
await drag(beside.split.x, beside.split.y, beside.split.x - 80, beside.split.y, { steps: 8 });
|
||||||
await sleep(200);
|
|
||||||
const widened = await json(`{
|
const widened = await json(`{
|
||||||
canvas: __gm.particles.canvasRect(),
|
canvas: __gm.particles.canvasRect(),
|
||||||
preview: __gm.particles.previewRect(),
|
preview: __gm.particles.previewRect(),
|
||||||
@@ -207,7 +191,6 @@ export async function particles(t) {
|
|||||||
JSON.stringify({ before: beside.preview.w, after: widened.preview.w, canvas: widened.canvas.w }),
|
JSON.stringify({ before: beside.preview.w, after: widened.preview.w, canvas: widened.canvas.w }),
|
||||||
);
|
);
|
||||||
await drag(beside.split.x - 80, beside.split.y, beside.split.x, beside.split.y, { steps: 8 });
|
await drag(beside.split.x - 80, beside.split.y, beside.split.x, beside.split.y, { steps: 8 });
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 31d-iv. (#86) A hold has to stop the clock, not just the picture: the library's second goes on
|
// 31d-iv. (#86) A hold has to stop the clock, not just the picture: the library's second goes on
|
||||||
// under the freeze, so letting go must move the panel's origin rather than resume at the
|
// under the freeze, so letting go must move the panel's origin rather than resume at the
|
||||||
@@ -217,12 +200,10 @@ export async function particles(t) {
|
|||||||
await evaluate('__gm.particles.hold(0.5)');
|
await evaluate('__gm.particles.hold(0.5)');
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
await clickText('Restart');
|
await clickText('Restart');
|
||||||
await sleep(200);
|
|
||||||
await evaluate('__gm.particleTime(2003)');
|
await evaluate('__gm.particleTime(2003)');
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
const waited = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
const waited = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||||
await clickText('Run');
|
await clickText('Run');
|
||||||
await sleep(200);
|
|
||||||
const letGo = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
const letGo = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||||
check(
|
check(
|
||||||
'three seconds spent on hold are not owed: letting it run again carries on from second 0',
|
'three seconds spent on hold are not owed: letting it run again carries on from second 0',
|
||||||
@@ -237,7 +218,6 @@ export async function particles(t) {
|
|||||||
a.props.lifetime = 2;
|
a.props.lifetime = 2;
|
||||||
a.props.rate = 5;
|
a.props.rate = 5;
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
await evaluate('__gm.particles.hold(1.5)');
|
await evaluate('__gm.particles.hold(1.5)');
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
const edited = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
const edited = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||||
@@ -254,7 +234,6 @@ export async function particles(t) {
|
|||||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(assetId)});
|
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(assetId)});
|
||||||
a.props.timeScale = 2;
|
a.props.timeScale = 2;
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
const scaled = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
const scaled = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||||
check(
|
check(
|
||||||
'a time scale of 2 halves what the slider spans and doubles what a held second shows',
|
'a time scale of 2 halves what the slider spans and doubles what a held second shows',
|
||||||
@@ -268,7 +247,6 @@ export async function particles(t) {
|
|||||||
delete a.props.rate;
|
delete a.props.rate;
|
||||||
delete a.props.timeScale;
|
delete a.props.timeScale;
|
||||||
`);
|
`);
|
||||||
await sleep(200);
|
|
||||||
await evaluate('__gm.particles.hold(null)');
|
await evaluate('__gm.particles.hold(null)');
|
||||||
await evaluate('__gm.particleTime(null)');
|
await evaluate('__gm.particleTime(null)');
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
@@ -298,8 +276,7 @@ export async function particles(t) {
|
|||||||
name: 'Fountain', position: { x: 0, y: ${LANE}, z: 0 }, editorOffset: { x: 0, y: 0 },
|
name: 'Fountain', position: { x: 0, y: ${LANE}, z: 0 }, editorOffset: { x: 0, y: 0 },
|
||||||
overrides: {}, parentId: '', instanceEdits: {} });
|
overrides: {}, parentId: '', instanceEdits: {} });
|
||||||
__gm.lookAt(0, ${LANE});`);
|
__gm.lookAt(0, ${LANE});`);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(400);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
const inScene = await drawn();
|
const inScene = await drawn();
|
||||||
@@ -312,8 +289,7 @@ export async function particles(t) {
|
|||||||
|
|
||||||
// 31f. Gravity is ½gt², not a force stepped frame by frame: the oldest particle, half a second
|
// 31f. Gravity is ½gt², not a force stepped frame by frame: the oldest particle, half a second
|
||||||
// old, is exactly 100 px below the source at strength 800.
|
// old, is exactly 100 px below the source at strength 800.
|
||||||
await evaluate("__gm.showTab('particles')");
|
await showTab('particles');
|
||||||
await sleep(250);
|
|
||||||
// Not a fixed point: the Output node is already on the canvas, and since the preview took the
|
// Not a fixed point: the Output node is already on the canvas, and since the preview took the
|
||||||
// right of the tab (#86) there is no corner left that is free by construction.
|
// right of the tab (#86) there is no corner left that is free by construction.
|
||||||
await placeNodeAt('Gravity', await json('__gm.particles.emptySpot()'));
|
await placeNodeAt('Gravity', await json('__gm.particles.emptySpot()'));
|
||||||
@@ -323,8 +299,7 @@ export async function particles(t) {
|
|||||||
const graph = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).graph;
|
const graph = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)}).graph;
|
||||||
graph.edges.push({ id: 'pt-e1', from: { node: ${JSON.stringify(gravityId)}, port: 'y' },
|
graph.edges.push({ id: 'pt-e1', from: { node: ${JSON.stringify(gravityId)}, port: 'y' },
|
||||||
to: { node: ${JSON.stringify(outputId)}, port: 'y' } });`);
|
to: { node: ${JSON.stringify(outputId)}, port: 'y' } });`);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
const falling = await drawn(2);
|
const falling = await drawn(2);
|
||||||
@@ -352,7 +327,6 @@ export async function particles(t) {
|
|||||||
await addNode('pt-age', 'ParticleAge');
|
await addNode('pt-age', 'ParticleAge');
|
||||||
await link('pt-e2', 'pt-age', 'age', 'pt-sub', 'b');
|
await link('pt-e2', 'pt-age', 'age', 'pt-sub', 'b');
|
||||||
await link('pt-e3', 'pt-sub', 'result', outputId, 'alpha');
|
await link('pt-e3', 'pt-sub', 'result', outputId, 'alpha');
|
||||||
await sleep(300);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
const fading = await drawn(11);
|
const fading = await drawn(11);
|
||||||
@@ -367,9 +341,7 @@ export async function particles(t) {
|
|||||||
// 31i. A Property node is a row on the placed source, like a shader parameter on a sprite.
|
// 31i. A Property node is a row on the placed source, like a shader parameter on a sprite.
|
||||||
await addNode('pt-prop', 'ParticleProperty', { name: 'drift', default: 0, min: -200, max: 200 });
|
await addNode('pt-prop', 'ParticleProperty', { name: 'drift', default: 0, min: -200, max: 200 });
|
||||||
await link('pt-e4', 'pt-prop', 'value', outputId, 'x');
|
await link('pt-e4', 'pt-prop', 'value', outputId, 'x');
|
||||||
await sleep(300);
|
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [SOURCE], objectId: SOURCE });
|
await select({ kind: 'object', holderId: 'scene1', objectIds: [SOURCE], objectId: SOURCE });
|
||||||
await sleep(350);
|
|
||||||
const driftRow = await json("__gm.inspectorField('drift')");
|
const driftRow = await json("__gm.inspectorField('drift')");
|
||||||
const rateRow = await json("__gm.inspectorField('Rate')");
|
const rateRow = await json("__gm.inspectorField('Rate')");
|
||||||
check(
|
check(
|
||||||
@@ -379,7 +351,6 @@ export async function particles(t) {
|
|||||||
);
|
);
|
||||||
await mutate(`
|
await mutate(`
|
||||||
__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)}).overrides.drift = 60;`);
|
__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)}).overrides.drift = 60;`);
|
||||||
await sleep(250);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
const drifted = await drawn(2);
|
const drifted = await drawn(2);
|
||||||
@@ -404,7 +375,6 @@ export async function particles(t) {
|
|||||||
);
|
);
|
||||||
await mutate(`
|
await mutate(`
|
||||||
__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)}).overrides.emitting = true;`);
|
__gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)}).overrides.emitting = true;`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
check('switching it back on fills the field again', (await drawn(1))?.count === RATE / 2 + 1, '');
|
check('switching it back on fills the field again', (await drawn(1))?.count === RATE / 2 + 1, '');
|
||||||
@@ -413,7 +383,6 @@ export async function particles(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
Object.assign(o.overrides, { mode: 'burst', count: 7 });`);
|
Object.assign(o.overrides, { mode: 'burst', count: 7 });`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
const burst = await drawn(7);
|
const burst = await drawn(7);
|
||||||
@@ -431,7 +400,6 @@ export async function particles(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
o.overrides.prewarm = true;`);
|
o.overrides.prewarm = true;`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
const warmBurst = await drawn(7);
|
const warmBurst = await drawn(7);
|
||||||
@@ -447,14 +415,12 @@ export async function particles(t) {
|
|||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
Object.assign(o.overrides, { mode: 'continuous', drift: 0 });
|
Object.assign(o.overrides, { mode: 'continuous', drift: 0 });
|
||||||
delete o.overrides.count;`);
|
delete o.overrides.count;`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0);
|
await at(0);
|
||||||
const warmStart = await drawn(1);
|
const warmStart = await drawn(1);
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
delete o.overrides.prewarm;`);
|
delete o.overrides.prewarm;`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0);
|
await at(0);
|
||||||
const coldStart = await drawn(1);
|
const coldStart = await drawn(1);
|
||||||
@@ -469,14 +435,12 @@ export async function particles(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
o.overrides.timeScale = 2;`);
|
o.overrides.timeScale = 2;`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.5);
|
await at(0.5);
|
||||||
const fast = await drawn(3);
|
const fast = await drawn(3);
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
delete o.overrides.timeScale;`);
|
delete o.overrides.timeScale;`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(1);
|
await at(1);
|
||||||
const plain = await drawn(3);
|
const plain = await drawn(3);
|
||||||
@@ -503,13 +467,11 @@ export async function particles(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
Object.assign(o.overrides, { mode: 'burst', count: 7 });`);
|
Object.assign(o.overrides, { mode: 'burst', count: 7 });`);
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 31l. It really draws: the pixel where a burst particle sits is not the scene background.
|
// 31l. It really draws: the pixel where a burst particle sits is not the scene background.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
Object.assign(o.overrides, { size: 40, color: 0x3cc85a, drift: 0 });`);
|
Object.assign(o.overrides, { size: 40, color: 0x3cc85a, drift: 0 });`);
|
||||||
await sleep(200);
|
|
||||||
await rewind();
|
await rewind();
|
||||||
await at(0.1);
|
await at(0.1);
|
||||||
const spotOn = await json(`__gm.objectScreen(${JSON.stringify(SOURCE)})`);
|
const spotOn = await json(`__gm.objectScreen(${JSON.stringify(SOURCE)})`);
|
||||||
@@ -531,9 +493,7 @@ export async function particles(t) {
|
|||||||
await sleep(400);
|
await sleep(400);
|
||||||
const thawed = await drawn(1);
|
const thawed = await drawn(1);
|
||||||
await t.play();
|
await t.play();
|
||||||
await sleep(300);
|
|
||||||
await t.stop();
|
await t.stop();
|
||||||
await sleep(400);
|
|
||||||
const afterRun = await drawn(1);
|
const afterRun = await drawn(1);
|
||||||
check(
|
check(
|
||||||
'the source draws on after the clock is pulled out from under it: a freeze released, a run ended',
|
'the source draws on after the clock is pulled out from under it: a freeze released, a run ended',
|
||||||
@@ -579,7 +539,6 @@ export async function particles(t) {
|
|||||||
const source = scene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const source = scene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
source.parentId = 'pt-anchor';
|
source.parentId = 'pt-anchor';
|
||||||
source.position = { x: 200, y: 0, z: 0 };`);
|
source.position = { x: 200, y: 0, z: 0 };`);
|
||||||
await sleep(300);
|
|
||||||
const linked = await json(`__gm.sceneIcon(${JSON.stringify(SOURCE)})`);
|
const linked = await json(`__gm.sceneIcon(${JSON.stringify(SOURCE)})`);
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const scene = __gm.state.activeScene;
|
const scene = __gm.state.activeScene;
|
||||||
@@ -587,7 +546,6 @@ export async function particles(t) {
|
|||||||
source.parentId = '';
|
source.parentId = '';
|
||||||
source.position = { x: 0, y: ${LANE}, z: 0 };
|
source.position = { x: 0, y: ${LANE}, z: 0 };
|
||||||
scene.objects = scene.objects.filter((o) => o.id !== 'pt-anchor');`);
|
scene.objects = scene.objects.filter((o) => o.id !== 'pt-anchor');`);
|
||||||
await sleep(300);
|
|
||||||
const unlinked = await json(`__gm.sceneIcon(${JSON.stringify(SOURCE)})`);
|
const unlinked = await json(`__gm.sceneIcon(${JSON.stringify(SOURCE)})`);
|
||||||
check(
|
check(
|
||||||
'a parented source hangs off its owner by a line, a free one by none',
|
'a parented source hangs off its owner by a line, a free one by none',
|
||||||
@@ -612,7 +570,6 @@ export async function particles(t) {
|
|||||||
});
|
});
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
Object.assign(o.overrides, { shader: ${JSON.stringify(SHADER)}, level: 1 });`);
|
Object.assign(o.overrides, { shader: ${JSON.stringify(SHADER)}, level: 1 });`);
|
||||||
await sleep(300);
|
|
||||||
await selectSource();
|
await selectSource();
|
||||||
// The tab bar has a button reading "Shader" too, and it is drawn first: the row wanted here is the
|
// The tab bar has a button reading "Shader" too, and it is drawn first: the row wanted here is the
|
||||||
// *last* text of that name, and it has to stand in the inspector's own column to be one.
|
// *last* text of that name, and it has to stand in the inspector's own column to be one.
|
||||||
@@ -641,7 +598,6 @@ export async function particles(t) {
|
|||||||
await mutate(`
|
await mutate(`
|
||||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||||
o.overrides.level = 0.25;`);
|
o.overrides.level = 0.25;`);
|
||||||
await sleep(300);
|
|
||||||
await at(0.1);
|
await at(0.1);
|
||||||
const dimmed = await json(`__gm.pixelAt(${Math.round(spotOn.x)}, ${Math.round(spotOn.y)})`);
|
const dimmed = await json(`__gm.pixelAt(${Math.round(spotOn.x)}, ${Math.round(spotOn.y)})`);
|
||||||
check(
|
check(
|
||||||
@@ -657,7 +613,6 @@ export async function particles(t) {
|
|||||||
const shader = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(SHADER)});
|
const shader = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(SHADER)});
|
||||||
shader.graph.nodes.find((n) => n.id === 'lv-prop').params.name = 'rate';
|
shader.graph.nodes.find((n) => n.id === 'lv-prop').params.name = 'rate';
|
||||||
shader.graph.edges = [];`);
|
shader.graph.edges = [];`);
|
||||||
await sleep(300);
|
|
||||||
await select(null);
|
await select(null);
|
||||||
await selectSource();
|
await selectSource();
|
||||||
const own = await json('__gm.locateAll("Rate")');
|
const own = await json('__gm.locateAll("Rate")');
|
||||||
@@ -673,7 +628,6 @@ export async function particles(t) {
|
|||||||
delete o.overrides.level;
|
delete o.overrides.level;
|
||||||
__gm.state.project.assets = __gm.state.project.assets.filter(
|
__gm.state.project.assets = __gm.state.project.assets.filter(
|
||||||
(a) => a.id !== ${JSON.stringify(SHADER)});`);
|
(a) => a.id !== ${JSON.stringify(SHADER)});`);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 31p. The graph tab draws no Pixi complaint of its own.
|
// 31p. The graph tab draws no Pixi complaint of its own.
|
||||||
const logged = await json('__gm.console.lines()');
|
const logged = await json('__gm.console.lines()');
|
||||||
@@ -692,8 +646,7 @@ export async function particles(t) {
|
|||||||
__gm.state.activeScene.objects = __gm.state.activeScene.objects.filter(
|
__gm.state.activeScene.objects = __gm.state.activeScene.objects.filter(
|
||||||
(o) => o.id !== ${JSON.stringify(SOURCE)});
|
(o) => o.id !== ${JSON.stringify(SOURCE)});
|
||||||
__gm.lookAt(0, 0);`);
|
__gm.lookAt(0, 0);`);
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing of its own behind',
|
'the step leaves nothing of its own behind',
|
||||||
(await json("__gm.state.project.assets.filter((a) => a.type === 'particles').length")) === 0 &&
|
(await json("__gm.state.project.assets.filter((a) => a.type === 'particles').length")) === 0 &&
|
||||||
|
|||||||
+3
-14
@@ -27,23 +27,18 @@ const counterGraph = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function scope(t) {
|
export async function scope(t) {
|
||||||
const { check, sleep, evaluate, json, shot, click, locate, asset, mutate, stop, has } = t;
|
const { check, sleep, json, shot, click, locate, asset, mutate, stop, has } = t;
|
||||||
|
|
||||||
const selectAsset = async (id) => {
|
const selectAsset = async (id) => {
|
||||||
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(id)} })`);
|
await t.select({ kind: 'asset', assetId: id });
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
const selectNode = async (nodeId) => {
|
const selectNode = async (nodeId) => {
|
||||||
await evaluate(
|
await t.select({ kind: 'node', assetId: 'sc-logic', nodeId });
|
||||||
`__gm.state.select({ kind: 'node', assetId: 'sc-logic', nodeId: ${JSON.stringify(nodeId)} })`,
|
|
||||||
);
|
|
||||||
await sleep(300);
|
|
||||||
};
|
};
|
||||||
/** The inspector is drawn over the graph, so the last match is the row rather than a port. */
|
/** The inspector is drawn over the graph, so the last match is the row rather than a port. */
|
||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(250);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 32a. A Variables asset, a behavior counting on it, and two boxes wearing that behavior.
|
// 32a. A Variables asset, a behavior counting on it, and two boxes wearing that behavior.
|
||||||
@@ -74,7 +69,6 @@ export async function scope(t) {
|
|||||||
make('sc-brain-b', 'sc-logic', 'sc-box-b', 0, 'Counter B'),
|
make('sc-brain-b', 'sc-logic', 'sc-box-b', 0, 'Counter B'),
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 32b. The run: two instances, one number. A per-instance variable would stand at 1.
|
// 32b. The run: two instances, one number. A per-instance variable would stand at 1.
|
||||||
await t.play();
|
await t.play();
|
||||||
@@ -93,7 +87,6 @@ export async function scope(t) {
|
|||||||
`objects carrying "score": ${JSON.stringify(carriers)}`,
|
`objects carrying "score": ${JSON.stringify(carriers)}`,
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 32c. The asset is where the declaration lives, and it says what it is for.
|
// 32c. The asset is where the declaration lives, and it says what it is for.
|
||||||
await selectAsset('sc-vars');
|
await selectAsset('sc-vars');
|
||||||
@@ -123,7 +116,6 @@ export async function scope(t) {
|
|||||||
const g = __gm.state.project.assets.find((a) => a.id === 'sc-logic').graph;
|
const g = __gm.state.project.assets.find((a) => a.id === 'sc-logic').graph;
|
||||||
g.nodes.find((n) => n.id === 'sc-read').params.variablesAsset = '';
|
g.nodes.find((n) => n.id === 'sc-read').params.variablesAsset = '';
|
||||||
`);
|
`);
|
||||||
await sleep(400);
|
|
||||||
const complainsNow = await has('"score" is not a variable of this behavior.');
|
const complainsNow = await has('"score" is not a variable of this behavior.');
|
||||||
check(
|
check(
|
||||||
'and clearing it points the node back at the behavior, which has no such variable',
|
'and clearing it points the node back at the behavior, which has no such variable',
|
||||||
@@ -146,9 +138,7 @@ export async function scope(t) {
|
|||||||
const machineScope = await has('Local');
|
const machineScope = await has('Local');
|
||||||
const pick = await locate('Local', true);
|
const pick = await locate('Local', true);
|
||||||
await click(pick.x, pick.y);
|
await click(pick.x, pick.y);
|
||||||
await sleep(250);
|
|
||||||
await clickLast('Global');
|
await clickLast('Global');
|
||||||
await sleep(350);
|
|
||||||
const machine = await asset('sc-machine');
|
const machine = await asset('sc-machine');
|
||||||
check(
|
check(
|
||||||
'a state machine is switched between one current state per object and one for the run',
|
'a state machine is switched between one current state per object and one for the run',
|
||||||
@@ -165,5 +155,4 @@ export async function scope(t) {
|
|||||||
p.assets = p.assets.filter((a) => !a.id.startsWith('sc-'));
|
p.assets = p.assets.filter((a) => !a.id.startsWith('sc-'));
|
||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
`);
|
`);
|
||||||
await sleep(300);
|
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import { openTab } from './lib/scaffold.mjs';
|
|||||||
* The step runs last and leaves the editor as it found it.
|
* The step runs last and leaves the editor as it found it.
|
||||||
*/
|
*/
|
||||||
export async function renderOnChange(t) {
|
export async function renderOnChange(t) {
|
||||||
const { check, sleep, evaluate, select, click, locate, play, stop, shot } = t;
|
const { check, sleep, evaluate, select, click, locate, play, stop, shot, waitUntil } = t;
|
||||||
|
|
||||||
const stats = () => evaluate('__gm.renderStats()');
|
const stats = () => evaluate('__gm.renderStats()');
|
||||||
/** Frames drawn over `ms` of doing nothing at all. */
|
/** Frames drawn over `ms` of doing nothing at all. */
|
||||||
@@ -23,8 +23,18 @@ export async function renderOnChange(t) {
|
|||||||
/**
|
/**
|
||||||
* Waits for the trailing window of the last event to run out. Measuring without this counts the
|
* 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.
|
* 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 = 3000) => {
|
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) {
|
for (let waited = 0; waited < timeoutMs; waited += 100) {
|
||||||
if (!(await stats()).awake) return true;
|
if (!(await stats()).awake) return true;
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
|
|||||||
+2
-22
@@ -19,7 +19,7 @@ const BLIP_BYTES = 4044;
|
|||||||
const DRONE_BYTES = 12044;
|
const DRONE_BYTES = 12044;
|
||||||
|
|
||||||
export async function sound(t) {
|
export async function sound(t) {
|
||||||
const { check, sleep, json, evaluate, mutate, click, clickText, press, play, stop, shot } = t;
|
const { check, sleep, json, evaluate, mutate, click, clickText, press, play, stop, shot, showTab } = t;
|
||||||
const { select } = t;
|
const { select } = t;
|
||||||
const assetsOfType = (type) =>
|
const assetsOfType = (type) =>
|
||||||
json(`__gm.state.project.assets.filter((a) => a.type === ${JSON.stringify(type)})
|
json(`__gm.state.project.assets.filter((a) => a.type === ${JSON.stringify(type)})
|
||||||
@@ -38,8 +38,7 @@ export async function sound(t) {
|
|||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// 34a. What the server holds, straight from the API: the two files the seed put there.
|
// 34a. What the server holds, straight from the API: the two files the seed put there.
|
||||||
const stored = await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`);
|
const stored = await evaluate(`fetch('/game-maker/api/index.php?r=sounds').then((r) => r.json())`);
|
||||||
@@ -59,7 +58,6 @@ export async function sound(t) {
|
|||||||
|
|
||||||
// 34b. "+ → Sound" offers them; a sound is picked rather than created empty, like a typeface.
|
// 34b. "+ → Sound" offers them; a sound is picked rather than created empty, like a typeface.
|
||||||
await clickText('+');
|
await clickText('+');
|
||||||
await sleep(200);
|
|
||||||
await pickAssetType(t, 'Sound');
|
await pickAssetType(t, 'Sound');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const picker = await json('__gm.soundPicker()');
|
const picker = await json('__gm.soundPicker()');
|
||||||
@@ -91,9 +89,7 @@ export async function sound(t) {
|
|||||||
|
|
||||||
const row = await json(`__gm.soundPickerRow(${JSON.stringify(blip.file)})`);
|
const row = await json(`__gm.soundPickerRow(${JSON.stringify(blip.file)})`);
|
||||||
await click(row.x, row.y);
|
await click(row.x, row.y);
|
||||||
await sleep(200);
|
|
||||||
await clickText('Use sound');
|
await clickText('Use sound');
|
||||||
await sleep(400);
|
|
||||||
const sounds = await assetsOfType('sound');
|
const sounds = await assetsOfType('sound');
|
||||||
check(
|
check(
|
||||||
'picking a row makes a sound asset of that file, named after the file rather than "Sound"',
|
'picking a row makes a sound asset of that file, named after the file rather than "Sound"',
|
||||||
@@ -116,7 +112,6 @@ export async function sound(t) {
|
|||||||
|
|
||||||
// 34d. Its inspector: the two rows, and the preview that is the only way to tell which sound it is.
|
// 34d. Its inspector: the two rows, and the preview that is the only way to tell which sound it is.
|
||||||
await select({ kind: 'asset', assetId: soundId });
|
await select({ kind: 'asset', assetId: soundId });
|
||||||
await sleep(300);
|
|
||||||
const volumeRow = await json("__gm.inspectorField('Volume', true)");
|
const volumeRow = await json("__gm.inspectorField('Volume', true)");
|
||||||
const loopRow = await json("__gm.inspectorField('Loop', true)");
|
const loopRow = await json("__gm.inspectorField('Loop', true)");
|
||||||
check(
|
check(
|
||||||
@@ -153,7 +148,6 @@ export async function sound(t) {
|
|||||||
make('sn-brain', 'sn-logic', 'sn-box', 0, 'Noisy'),
|
make('sn-brain', 'sn-logic', 'sn-box', 0, 'Noisy'),
|
||||||
);
|
);
|
||||||
__gm.lookAt(0, ${LANE});`);
|
__gm.lookAt(0, ${LANE});`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
await evaluate('__gm.sound.stopAll()');
|
await evaluate('__gm.sound.stopAll()');
|
||||||
const before = await stats();
|
const before = await stats();
|
||||||
@@ -168,14 +162,12 @@ export async function sound(t) {
|
|||||||
JSON.stringify(played),
|
JSON.stringify(played),
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 34f. A looping sound is music: it plays on, and ending the run ends it – nothing may outlive
|
// 34f. A looping sound is music: it plays on, and ending the run ends it – nothing may outlive
|
||||||
// the world that started it.
|
// the world that started it.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const p = __gm.state.project;
|
const p = __gm.state.project;
|
||||||
p.assets.find((a) => a.id === ${JSON.stringify(soundId)}).props.loop = true;`);
|
p.assets.find((a) => a.id === ${JSON.stringify(soundId)}).props.loop = true;`);
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
const looping = await waitFor(stats, (s) => s.playing > 0 && s.last?.loop === true);
|
const looping = await waitFor(stats, (s) => s.playing > 0 && s.last?.loop === true);
|
||||||
check(
|
check(
|
||||||
@@ -187,7 +179,6 @@ export async function sound(t) {
|
|||||||
await sleep(700);
|
await sleep(700);
|
||||||
const stillLooping = await stats();
|
const stillLooping = await stats();
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(400);
|
|
||||||
const afterStop = await stats();
|
const afterStop = await stats();
|
||||||
check(
|
check(
|
||||||
'a loop keeps playing while the run is on and ends with it',
|
'a loop keeps playing while the run is on and ends with it',
|
||||||
@@ -203,15 +194,12 @@ export async function sound(t) {
|
|||||||
const graph = p.assets.find((a) => a.id === 'sn-logic').graph;
|
const graph = p.assets.find((a) => a.id === 'sn-logic').graph;
|
||||||
graph.nodes.find((n) => n.id === 'sn-play').params.loop = ${JSON.stringify(value)};`);
|
graph.nodes.find((n) => n.id === 'sn-play').params.loop = ${JSON.stringify(value)};`);
|
||||||
await setNodeLoop('false');
|
await setNodeLoop('false');
|
||||||
await sleep(300);
|
|
||||||
const beforeOnce = await stats();
|
const beforeOnce = await stats();
|
||||||
await play();
|
await play();
|
||||||
const once = await waitFor(stats, (s) => s.started > beforeOnce.started);
|
const once = await waitFor(stats, (s) => s.started > beforeOnce.started);
|
||||||
check('a node saying "off" plays a looping asset once', once.last?.loop === false, JSON.stringify(once));
|
check('a node saying "off" plays a looping asset once', once.last?.loop === false, JSON.stringify(once));
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
await setNodeLoop('');
|
await setNodeLoop('');
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 34g. `Stop sound` silences it from inside the graph, without ending the run.
|
// 34g. `Stop sound` silences it from inside the graph, without ending the run.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
@@ -220,7 +208,6 @@ export async function sound(t) {
|
|||||||
graph.nodes.push(${JSON.stringify(node('sn-stop', 'StopSound', 520, 0, { sound: soundId }))});
|
graph.nodes.push(${JSON.stringify(node('sn-stop', 'StopSound', 520, 0, { sound: soundId }))});
|
||||||
graph.nodes.push(${JSON.stringify(node('sn-key', 'OnKeyDown', 260, 220, { key: 's' }))});
|
graph.nodes.push(${JSON.stringify(node('sn-key', 'OnKeyDown', 260, 220, { key: 's' }))});
|
||||||
graph.edges.push(${JSON.stringify(edge('sn-e2', 'sn-key', 'exec', 'sn-stop', 'exec'))});`);
|
graph.edges.push(${JSON.stringify(edge('sn-e2', 'sn-key', 'exec', 'sn-stop', 'exec'))});`);
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
await waitFor(stats, (s) => s.playing > 0);
|
await waitFor(stats, (s) => s.playing > 0);
|
||||||
await press('s');
|
await press('s');
|
||||||
@@ -231,7 +218,6 @@ export async function sound(t) {
|
|||||||
JSON.stringify(silenced),
|
JSON.stringify(silenced),
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 34g-2. A voice belongs to the behavior playing it: destroy the object and its sounds go with it,
|
// 34g-2. A voice belongs to the behavior playing it: destroy the object and its sounds go with it,
|
||||||
// without any `Stop sound` in the graph. `detached` is the one way out, for the death
|
// without any `Stop sound` in the graph. `detached` is the one way out, for the death
|
||||||
@@ -248,7 +234,6 @@ export async function sound(t) {
|
|||||||
${JSON.stringify(edge('sn-e3', 'sn-key2', 'exec', 'sn-destroy', 'exec'))},
|
${JSON.stringify(edge('sn-e3', 'sn-key2', 'exec', 'sn-destroy', 'exec'))},
|
||||||
${JSON.stringify(edge('sn-e4', 'sn-owner', 'object', 'sn-destroy', 'object'))},
|
${JSON.stringify(edge('sn-e4', 'sn-owner', 'object', 'sn-destroy', 'object'))},
|
||||||
);`);
|
);`);
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
await waitFor(stats, (s) => s.playing > 0);
|
await waitFor(stats, (s) => s.playing > 0);
|
||||||
await press('d');
|
await press('d');
|
||||||
@@ -259,7 +244,6 @@ export async function sound(t) {
|
|||||||
JSON.stringify(destroyed),
|
JSON.stringify(destroyed),
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
const setDetached = (value) =>
|
const setDetached = (value) =>
|
||||||
mutate(`
|
mutate(`
|
||||||
@@ -267,7 +251,6 @@ export async function sound(t) {
|
|||||||
const graph = p.assets.find((a) => a.id === 'sn-logic').graph;
|
const graph = p.assets.find((a) => a.id === 'sn-logic').graph;
|
||||||
graph.nodes.find((n) => n.id === 'sn-play').params.detached = ${JSON.stringify(value)};`);
|
graph.nodes.find((n) => n.id === 'sn-play').params.detached = ${JSON.stringify(value)};`);
|
||||||
await setDetached(true);
|
await setDetached(true);
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
await waitFor(stats, (s) => s.playing > 0);
|
await waitFor(stats, (s) => s.playing > 0);
|
||||||
await press('d');
|
await press('d');
|
||||||
@@ -280,14 +263,12 @@ export async function sound(t) {
|
|||||||
JSON.stringify(detached),
|
JSON.stringify(detached),
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'ending the run silences a detached voice too',
|
'ending the run silences a detached voice too',
|
||||||
(await stats()).playing === 0,
|
(await stats()).playing === 0,
|
||||||
JSON.stringify(await stats()),
|
JSON.stringify(await stats()),
|
||||||
);
|
);
|
||||||
await setDetached(false);
|
await setDetached(false);
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 34h. Dropping a sound file onto the editor uploads it and makes an asset named after the file.
|
// 34h. Dropping a sound file onto the editor uploads it and makes an asset named after the file.
|
||||||
const dropped = await evaluate(`
|
const dropped = await evaluate(`
|
||||||
@@ -322,7 +303,6 @@ export async function sound(t) {
|
|||||||
(o) => !['sn-box', 'sn-brain'].includes(o.id));
|
(o) => !['sn-box', 'sn-brain'].includes(o.id));
|
||||||
__gm.state.select(null);
|
__gm.state.select(null);
|
||||||
__gm.lookAt(0, 0);`);
|
__gm.lookAt(0, 0);`);
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing of its own behind',
|
'the step leaves nothing of its own behind',
|
||||||
(await assetsOfType('sound')).length === 0 && (await stats()).playing === 0,
|
(await assetsOfType('sound')).length === 0 && (await stats()).playing === 0,
|
||||||
|
|||||||
+17
-15
@@ -35,14 +35,28 @@ const graphFor = (sceneId, soundId) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export async function loadScene(t) {
|
export async function loadScene(t) {
|
||||||
const { check, sleep, json, evaluate, mutate, click, clickText, locate, press, play, stop, shot, has } = t;
|
const {
|
||||||
|
check,
|
||||||
|
sleep,
|
||||||
|
json,
|
||||||
|
evaluate,
|
||||||
|
mutate,
|
||||||
|
click,
|
||||||
|
clickText,
|
||||||
|
locate,
|
||||||
|
press,
|
||||||
|
play,
|
||||||
|
stop,
|
||||||
|
shot,
|
||||||
|
has,
|
||||||
|
showTab,
|
||||||
|
} = t;
|
||||||
const playInfo = () => json('__gm.playInfo()');
|
const playInfo = () => json('__gm.playInfo()');
|
||||||
const playFrom = () => json('__gm.playFrom()');
|
const playFrom = () => json('__gm.playFrom()');
|
||||||
/** The switch is opened by position: its caption is elided at the width it has. */
|
/** The switch is opened by position: its caption is elided at the width it has. */
|
||||||
const openPlayFrom = async () => {
|
const openPlayFrom = async () => {
|
||||||
const at = await json('__gm.playFromAt()');
|
const at = await json('__gm.playFromAt()');
|
||||||
await click(at.x, at.y);
|
await click(at.x, at.y);
|
||||||
await sleep(250);
|
|
||||||
};
|
};
|
||||||
const stats = () => json('__gm.sound.stats()');
|
const stats = () => json('__gm.sound.stats()');
|
||||||
const waitFor = async (read, done, tries = 25) => {
|
const waitFor = async (read, done, tries = 25) => {
|
||||||
@@ -56,8 +70,7 @@ export async function loadScene(t) {
|
|||||||
|
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await evaluate('__gm.state.select(null)');
|
await evaluate('__gm.state.select(null)');
|
||||||
await evaluate("__gm.showTab('scene')");
|
await showTab('scene');
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
const firstId = await json('__gm.state.project.activeSceneId');
|
const firstId = await json('__gm.state.project.activeSceneId');
|
||||||
const firstName = await json('__gm.state.activeScene.name');
|
const firstName = await json('__gm.state.activeScene.name');
|
||||||
@@ -100,7 +113,6 @@ export async function loadScene(t) {
|
|||||||
props: { volume: 1, loop: true },
|
props: { volume: 1, loop: true },
|
||||||
file: ${JSON.stringify(file)},
|
file: ${JSON.stringify(file)},
|
||||||
});`);
|
});`);
|
||||||
await sleep(400);
|
|
||||||
|
|
||||||
// 35b. Without a start scene the switch says the active scene – the two questions have the same
|
// 35b. Without a start scene the switch says the active scene – the two questions have the same
|
||||||
// answer until someone answers them differently.
|
// answer until someone answers them differently.
|
||||||
@@ -118,16 +130,13 @@ export async function loadScene(t) {
|
|||||||
// project holds every kind of asset, so the new scene is the last row of a list far longer
|
// project holds every kind of asset, so the new scene is the last row of a list far longer
|
||||||
// than the panel – selecting it brings it into view, which is what makes the click land.
|
// than the panel – selecting it brings it into view, which is what makes the click land.
|
||||||
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'ls-scene' })");
|
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'ls-scene' })");
|
||||||
await sleep(200);
|
|
||||||
check(
|
check(
|
||||||
'selecting an asset scrolls its row into the panel',
|
'selecting an asset scrolls its row into the panel',
|
||||||
await has('Level 2'),
|
await has('Level 2'),
|
||||||
JSON.stringify({ row: await json('__gm.assetRow("ls-scene")'), panel: await json('__gm.assetsRect()') }),
|
JSON.stringify({ row: await json('__gm.assetRow("ls-scene")'), panel: await json('__gm.assetsRect()') }),
|
||||||
);
|
);
|
||||||
await clickText('Level 2', 'right');
|
await clickText('Level 2', 'right');
|
||||||
await sleep(250);
|
|
||||||
await clickText('Set as start scene');
|
await clickText('Set as start scene');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the scene’s context menu names it the start scene, and the switch follows',
|
'the scene’s context menu names it the start scene, and the switch follows',
|
||||||
(await json('__gm.state.project.startSceneId')) === 'ls-scene' && (await playFrom()).scene === 'Level 2',
|
(await json('__gm.state.project.startSceneId')) === 'ls-scene' && (await playFrom()).scene === 'Level 2',
|
||||||
@@ -158,12 +167,10 @@ export async function loadScene(t) {
|
|||||||
// and not the other was the inconsistency. The run itself is untouched by it.
|
// and not the other was the inconsistency. The run itself is untouched by it.
|
||||||
const select = await json('__gm.sceneSelect()');
|
const select = await json('__gm.sceneSelect()');
|
||||||
await click(select.x, select.y);
|
await click(select.x, select.y);
|
||||||
await sleep(250);
|
|
||||||
const rows = await json('__gm.menuRows()');
|
const rows = await json('__gm.menuRows()');
|
||||||
// The topmost hit, because the scene's name is on its row in the Assets panel as well.
|
// The topmost hit, because the scene's name is on its row in the Assets panel as well.
|
||||||
const entry = await locate('Level 2', true);
|
const entry = await locate('Level 2', true);
|
||||||
await click(entry.x, entry.y);
|
await click(entry.x, entry.y);
|
||||||
await sleep(300);
|
|
||||||
const playing = await playInfo();
|
const playing = await playInfo();
|
||||||
check(
|
check(
|
||||||
'the scene selector stays usable in play mode, and the run runs on',
|
'the scene selector stays usable in play mode, and the run runs on',
|
||||||
@@ -208,13 +215,11 @@ export async function loadScene(t) {
|
|||||||
JSON.stringify({ humming, now: await stats() }),
|
JSON.stringify({ humming, now: await stats() }),
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// 35f. The switch is the way to run what you are working on instead – and it is remembered
|
// 35f. The switch is the way to run what you are working on instead – and it is remembered
|
||||||
// between runs, because it says how you work rather than what the game does.
|
// between runs, because it says how you work rather than what the game does.
|
||||||
await openPlayFrom();
|
await openPlayFrom();
|
||||||
await clickText('Active scene');
|
await clickText('Active scene');
|
||||||
await sleep(300);
|
|
||||||
await play();
|
await play();
|
||||||
const fromActive = await playInfo();
|
const fromActive = await playInfo();
|
||||||
check(
|
check(
|
||||||
@@ -226,10 +231,8 @@ export async function loadScene(t) {
|
|||||||
JSON.stringify({ fromActive, switch: await playFrom() }),
|
JSON.stringify({ fromActive, switch: await playFrom() }),
|
||||||
);
|
);
|
||||||
await stop();
|
await stop();
|
||||||
await sleep(300);
|
|
||||||
await openPlayFrom();
|
await openPlayFrom();
|
||||||
await clickText('Start scene');
|
await clickText('Start scene');
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the switch goes back to the start scene',
|
'the switch goes back to the start scene',
|
||||||
(await playFrom()).mode === 'start' && (await playFrom()).scene === 'Level 2',
|
(await playFrom()).mode === 'start' && (await playFrom()).scene === 'Level 2',
|
||||||
@@ -243,7 +246,6 @@ export async function loadScene(t) {
|
|||||||
const gone = new Set(['ls-logic', 'ls-scene', 'ls-vars', 'ls-sound']);
|
const gone = new Set(['ls-logic', 'ls-scene', 'ls-vars', 'ls-sound']);
|
||||||
p.assets = p.assets.filter((a) => !gone.has(a.id));
|
p.assets = p.assets.filter((a) => !gone.has(a.id));
|
||||||
__gm.state.select(null);`);
|
__gm.state.select(null);`);
|
||||||
await sleep(300);
|
|
||||||
check(
|
check(
|
||||||
'the step leaves nothing of its own behind',
|
'the step leaves nothing of its own behind',
|
||||||
(await json('__gm.state.project.assets.filter((a) => a.id.startsWith("ls-")).length')) === 0 &&
|
(await json('__gm.state.project.assets.filter((a) => a.id.startsWith("ls-")).length')) === 0 &&
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ export async function scaffoldBehavior(t, options) {
|
|||||||
make(${JSON.stringify(logicId)}, ${JSON.stringify(assetId)}, ${JSON.stringify(boxId)}, 0, ${JSON.stringify(name)}),
|
make(${JSON.stringify(logicId)}, ${JSON.stringify(assetId)}, ${JSON.stringify(boxId)}, 0, ${JSON.stringify(name)}),
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
await t.sleep(400);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetId,
|
assetId,
|
||||||
@@ -93,7 +92,6 @@ export async function scaffoldBehavior(t, options) {
|
|||||||
scene.objects = scene.objects.filter((o) => !objects.includes(o.id));
|
scene.objects = scene.objects.filter((o) => !objects.includes(o.id));
|
||||||
p.assets = p.assets.filter((a) => !assets.includes(a.id));
|
p.assets = p.assets.filter((a) => !assets.includes(a.id));
|
||||||
`);
|
`);
|
||||||
await t.sleep(300);
|
|
||||||
return t.json(`{
|
return t.json(`{
|
||||||
objects: __gm.sceneObjects().filter((o) => ${JSON.stringify([boxId, logicId])}.includes(o.id)).length,
|
objects: __gm.sceneObjects().filter((o) => ${JSON.stringify([boxId, logicId])}.includes(o.id)).length,
|
||||||
assets: __gm.state.project.assets.filter((a) => a.id === ${JSON.stringify(assetId)}).length,
|
assets: __gm.state.project.assets.filter((a) => a.id === ${JSON.stringify(assetId)}).length,
|
||||||
@@ -125,10 +123,8 @@ export async function openTab(t, tab) {
|
|||||||
const row = await t.json('__gm.tabRow()');
|
const row = await t.json('__gm.tabRow()');
|
||||||
if (row.hidden.includes(tab)) {
|
if (row.hidden.includes(tab)) {
|
||||||
await t.clickText('»');
|
await t.clickText('»');
|
||||||
await t.sleep(300);
|
|
||||||
}
|
}
|
||||||
await t.clickText(TAB_LABELS[tab]);
|
await t.clickText(TAB_LABELS[tab]);
|
||||||
await t.sleep(400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,11 +140,9 @@ export async function pickAssetType(t, label) {
|
|||||||
let scrolled = false;
|
let scrolled = false;
|
||||||
if (box && (!p || p.y > box.y + box.h - 4)) {
|
if (box && (!p || p.y > box.y + box.h - 4)) {
|
||||||
await t.wheel(box.x + box.w / 2, box.y + box.h / 2, 300);
|
await t.wheel(box.x + box.w / 2, box.y + box.h / 2, 300);
|
||||||
await t.sleep(250);
|
|
||||||
p = await t.locate(label, true);
|
p = await t.locate(label, true);
|
||||||
scrolled = true;
|
scrolled = true;
|
||||||
}
|
}
|
||||||
await t.click(p.x, p.y);
|
await t.click(p.x, p.y);
|
||||||
await t.sleep(250);
|
|
||||||
return { box, p, scrolled };
|
return { box, p, scrolled };
|
||||||
}
|
}
|
||||||
+86
-12
@@ -6,7 +6,17 @@
|
|||||||
import { writeFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
import { launchBrowser } from './browser.mjs';
|
import { launchBrowser } from './browser.mjs';
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
/**
|
||||||
|
* Every wait the checks do goes through here, and the time is booked. A run that takes ten
|
||||||
|
* minutes needs to say how much of that was the editor working and how much was the harness
|
||||||
|
* waiting for a deadline it made up – the two call for opposite fixes.
|
||||||
|
*/
|
||||||
|
export const waited = { ms: 0, calls: 0 };
|
||||||
|
const sleep = (ms) => {
|
||||||
|
waited.ms += ms;
|
||||||
|
waited.calls++;
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
};
|
||||||
|
|
||||||
/** Generous on purpose: a slow navigation still finishes, a wedged chromium never does. */
|
/** Generous on purpose: a slow navigation still finishes, a wedged chromium never does. */
|
||||||
const CDP_TIMEOUT_MS = 30_000;
|
const CDP_TIMEOUT_MS = 30_000;
|
||||||
@@ -131,6 +141,41 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
};
|
};
|
||||||
/** Evaluates an expression in the page and returns it as plain JSON; `undefined` comes back as null. */
|
/** Evaluates an expression in the page and returns it as plain JSON; `undefined` comes back as null. */
|
||||||
const json = async (expression) => JSON.parse(await evaluate(`JSON.stringify((${expression}) ?? null)`));
|
const json = async (expression) => JSON.parse(await evaluate(`JSON.stringify((${expression}) ?? null)`));
|
||||||
|
/**
|
||||||
|
* Waits for the editor to have drawn, instead of for a deadline someone guessed. A frame costs
|
||||||
|
* ~16 ms where the fixed waits this replaces cost 150–300, and it is the editor itself that
|
||||||
|
* says when it has caught up – so the same call is both faster and less flaky than the number
|
||||||
|
* it replaces. Two frames by default: one for the handler, one for the layout it invalidated.
|
||||||
|
*
|
||||||
|
* Falls back to a fixed wait when the page has no such hook (an old build, or before the first
|
||||||
|
* navigation), because a silent no-wait would turn every later check into a coin toss.
|
||||||
|
*/
|
||||||
|
const settle = async (count = 2) => {
|
||||||
|
const at = Date.now();
|
||||||
|
const ok = await evaluate(
|
||||||
|
`window.__gm?.afterFrames ? __gm.afterFrames(${count}).then(() => true) : false`,
|
||||||
|
);
|
||||||
|
if (!ok) return sleep(150);
|
||||||
|
waited.ms += Date.now() - at;
|
||||||
|
waited.calls++;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Waits until `expression` is true in the page – for the things a frame cannot answer, such as a
|
||||||
|
* save that is still in flight. Polls, because the conditions live all over the model and none
|
||||||
|
* of them has an event worth inventing one for.
|
||||||
|
*/
|
||||||
|
const waitUntil = async (expression, { timeout = 5000, label = expression } = {}) => {
|
||||||
|
const at = Date.now();
|
||||||
|
while (Date.now() - at < timeout) {
|
||||||
|
if (await json(`Boolean(${expression})`)) {
|
||||||
|
waited.ms += Date.now() - at;
|
||||||
|
waited.calls++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sleep(25);
|
||||||
|
}
|
||||||
|
throw new Error(`waited ${timeout} ms and ${label} never came true`);
|
||||||
|
};
|
||||||
/** Like `locate`, but null instead of throwing – which a check for absence needs. */
|
/** Like `locate`, but null instead of throwing – which a check for absence needs. */
|
||||||
const find = async (text, last = false) =>
|
const find = async (text, last = false) =>
|
||||||
(await evaluate(`window.__gm && window.__gm.locate(${JSON.stringify(text)}, ${last})`)) || null;
|
(await evaluate(`window.__gm && window.__gm.locate(${JSON.stringify(text)}, ${last})`)) || null;
|
||||||
@@ -163,7 +208,7 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
await mouse('mouseMoved', x, y);
|
await mouse('mouseMoved', x, y);
|
||||||
await mouse('mousePressed', x, y, button, extra);
|
await mouse('mousePressed', x, y, button, extra);
|
||||||
await mouse('mouseReleased', x, y, button, extra);
|
await mouse('mouseReleased', x, y, button, extra);
|
||||||
await sleep(150);
|
await settle();
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* A real double-click: two press/release pairs in quick succession, the second carrying
|
* A real double-click: two press/release pairs in quick succession, the second carrying
|
||||||
@@ -176,13 +221,13 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
await mouse('mousePressed', x, y, 'left', { clickCount });
|
await mouse('mousePressed', x, y, 'left', { clickCount });
|
||||||
await mouse('mouseReleased', x, y, 'left', { clickCount });
|
await mouse('mouseReleased', x, y, 'left', { clickCount });
|
||||||
}
|
}
|
||||||
await sleep(150);
|
await settle();
|
||||||
};
|
};
|
||||||
/** Mouse wheel over (x, y); positive `dy` scrolls down. */
|
/** Mouse wheel over (x, y); positive `dy` scrolls down. */
|
||||||
const wheel = async (x, y, dy) => {
|
const wheel = async (x, y, dy) => {
|
||||||
await mouse('mouseMoved', x, y);
|
await mouse('mouseMoved', x, y);
|
||||||
await mouse('mouseWheel', x, y, 'none', { deltaX: 0, deltaY: dy });
|
await mouse('mouseWheel', x, y, 'none', { deltaX: 0, deltaY: dy });
|
||||||
await sleep(150);
|
await settle();
|
||||||
};
|
};
|
||||||
const clickText = async (text, button = 'left') => {
|
const clickText = async (text, button = 'left') => {
|
||||||
const p = await locate(text);
|
const p = await locate(text);
|
||||||
@@ -206,7 +251,7 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
await sleep(16);
|
await sleep(16);
|
||||||
}
|
}
|
||||||
await mouse('mouseReleased', x2, y2, button, extra);
|
await mouse('mouseReleased', x2, y2, button, extra);
|
||||||
await sleep(150);
|
await settle();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,7 +276,7 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
await sleep(16);
|
await sleep(16);
|
||||||
}
|
}
|
||||||
await mouse('mouseReleased', x, y);
|
await mouse('mouseReleased', x, y);
|
||||||
await sleep(200);
|
await settle();
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyEvent = (type, name, modifiers) => {
|
const keyEvent = (type, name, modifiers) => {
|
||||||
@@ -243,6 +288,7 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
const press = async (name, modifiers = {}) => {
|
const press = async (name, modifiers = {}) => {
|
||||||
await keyEvent('keyDown', name, modifiers);
|
await keyEvent('keyDown', name, modifiers);
|
||||||
await keyEvent('keyUp', name, modifiers);
|
await keyEvent('keyUp', name, modifiers);
|
||||||
|
await settle();
|
||||||
};
|
};
|
||||||
/** Hold a key for `ms` milliseconds (arrow keys while playing). */
|
/** Hold a key for `ms` milliseconds (arrow keys while playing). */
|
||||||
const holdKey = async (name, ms) => {
|
const holdKey = async (name, ms) => {
|
||||||
@@ -267,14 +313,13 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
`typing "${text}" did not arrive: focus is ${typed.tag}, it holds ${JSON.stringify(typed.value)}`,
|
`typing "${text}" did not arrive: focus is ${typed.tag}, it holds ${JSON.stringify(typed.value)}`,
|
||||||
);
|
);
|
||||||
|
await settle();
|
||||||
};
|
};
|
||||||
const undo = async () => {
|
const undo = async () => {
|
||||||
await press('z', { ctrl: true });
|
await press('z', { ctrl: true });
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
const redo = async () => {
|
const redo = async () => {
|
||||||
await press('z', { ctrl: true, shift: true });
|
await press('z', { ctrl: true, shift: true });
|
||||||
await sleep(200);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Selection, mode, save status and notice – the facts most checks look at. */
|
/** Selection, mode, save status and notice – the facts most checks look at. */
|
||||||
@@ -294,22 +339,47 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
const asset = (id) => json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)})`);
|
const asset = (id) => json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)})`);
|
||||||
const world = () => json('__gm.world()');
|
const world = () => json('__gm.world()');
|
||||||
const entity = async (objectId) => (await world())?.entities.find((e) => e.objectId === objectId);
|
const entity = async (objectId) => (await world())?.entities.find((e) => e.objectId === objectId);
|
||||||
const select = (selection) => evaluate(`__gm.state.select(${JSON.stringify(selection)})`);
|
const select = async (selection) => {
|
||||||
|
await evaluate(`__gm.state.select(${JSON.stringify(selection)})`);
|
||||||
|
await settle();
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Switching the centre tab rebuilds a whole panel, so it needs the same wait an action gets.
|
||||||
|
* It is a helper rather than a bare `evaluate` in every step because a tab change without one
|
||||||
|
* reads the old panel – which is a check that passes on stale content, the worst kind.
|
||||||
|
*/
|
||||||
|
const showTab = async (tab) => {
|
||||||
|
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||||||
|
await settle();
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Waits for the autosave to have reached the server. The status goes `dirty` → `saving` →
|
||||||
|
* `saved`, and only the last one means the document a reload would fetch holds the change.
|
||||||
|
*/
|
||||||
|
const saved = () =>
|
||||||
|
waitUntil("__gm.state.saveStatus === 'saved'", { timeout: 9000, label: 'the save finished' });
|
||||||
/**
|
/**
|
||||||
* Runs `body` (JavaScript source) against the page's model and previews the result, the way a
|
* Runs `body` (JavaScript source) against the page's model and previews the result, the way a
|
||||||
* scrub or a command would – for putting the scene into a known state between checks.
|
* scrub or a command would – for putting the scene into a known state between checks.
|
||||||
*/
|
*/
|
||||||
const mutate = async (body) => {
|
const mutate = async (body) => {
|
||||||
await evaluate(`(() => { ${body}; __gm.state.preview(); })()`);
|
await evaluate(`(() => { ${body}; __gm.state.preview(); })()`);
|
||||||
await sleep(200);
|
await settle();
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Starting and stopping a run build or tear down a whole world, which is more than the two frames
|
||||||
|
* `settle()` grants – so these wait for the mode itself to have changed, then for a drawn frame so
|
||||||
|
* the panels that follow the mode are on screen too.
|
||||||
|
*/
|
||||||
const play = async () => {
|
const play = async () => {
|
||||||
await clickText('▶ Play');
|
await clickText('▶ Play');
|
||||||
await sleep(300);
|
await waitUntil("__gm.state.mode === 'play'", { label: 'the run started' });
|
||||||
|
await settle();
|
||||||
};
|
};
|
||||||
const stop = async () => {
|
const stop = async () => {
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(300);
|
await waitUntil("__gm.state.mode === 'edit'", { label: 'the run stopped' });
|
||||||
|
await settle();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Timeout, not convenience: a stalled response keeps its socket open, so node stays alive and
|
// Timeout, not convenience: a stalled response keeps its socket open, so node stays alive and
|
||||||
@@ -342,6 +412,8 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
sleep,
|
sleep,
|
||||||
evaluate,
|
evaluate,
|
||||||
json,
|
json,
|
||||||
|
settle,
|
||||||
|
waitUntil,
|
||||||
find,
|
find,
|
||||||
has,
|
has,
|
||||||
locate,
|
locate,
|
||||||
@@ -368,6 +440,8 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
|
|||||||
world,
|
world,
|
||||||
entity,
|
entity,
|
||||||
select,
|
select,
|
||||||
|
showTab,
|
||||||
|
saved,
|
||||||
mutate,
|
mutate,
|
||||||
play,
|
play,
|
||||||
stop,
|
stop,
|
||||||
|
|||||||
Reference in new issue
Block a user