The topbar is icons only: the leading Project dropdown, the Scene picker and the labelled Play/Pause buttons are gone and in their place stand five icon buttons - menu, undo, redo on the left, play and pause in the middle - while Notice, the save state, fps and the Account menu keep the right side; the Play button swaps its icon for a stop square while the project runs and Pause swaps for a second play sign while it is held (Button.setIcon()), and iconSpace() drops the gap between icon and label when a button has no label, which is what kept a 24 px icon button 3 px off its own centre; the menu carries New project, Open project..., a separator and Project settings..., where the picker (src/editor/OpenProjectDialog.ts) preselects the open project and marks it "open", opens on a double click and puts Delete on the left of Cancel and Open - it closes itself before it asks whether to delete, because two modals on top of each other would have left the question standing over a list it no longer speaks about - and the settings dialog (src/editor/ProjectSettingsDialog.ts) holds the name, the "Play starts" switch (Start scene / Active scene, a preference of this browser and so no command) and the collision layers from #152, which retires CollisionLayersDialog.ts and the "Collision layers..." entry; what Apply actually writes is the pure function settingsChanges(), so renaming and the layers are each their own undo step and each drop out when nothing differs; the Scene dropdown falls away without replacement, the active scene is switched by double clicking it in the Assets panel, which is the way that was there anyway; new module 46 runs last with 9 checks (no label left in the bar, the run buttons in edit mode, the four menu entries, the picker with the open project preselected, Cancel leaving the project standing, the settings dialog on the state of the project, renaming with undo and redo through the icon buttons) and it neither opens nor deletes a project, because that would pull the project out from under every earlier module; modules 8, 18 and 35 and the harness follow the new bar, 6 unit tests cover settingsChanges and 3 the icons (#183)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
defd4a86d2
commit
60d0146bde
24 files changed
+751
-284
No files matched your search
@@ -15,6 +15,7 @@ export async function colliders(t) {
|
||||
click,
|
||||
doubleClick,
|
||||
clickText,
|
||||
clickToolbar,
|
||||
locate,
|
||||
press,
|
||||
typeText,
|
||||
@@ -135,14 +136,15 @@ export async function colliders(t) {
|
||||
await shot('22-trigger-collider');
|
||||
|
||||
// 8d. Collision layers (#152). The project starts with the one layer every collider is on; a
|
||||
// second one is added and named in the dialog behind Project → "Collision layers…".
|
||||
await clickText('Project');
|
||||
// second one is added and named in the Project settings dialog, which is where the layers
|
||||
// moved with the top bar's rework (#183) – menu button → "Project settings…".
|
||||
await clickToolbar('menu');
|
||||
check(
|
||||
'the Project menu offers the collision layers',
|
||||
(await json('__gm.menuRows()')).includes('Collision layers…'),
|
||||
'the top bar menu offers the project settings',
|
||||
(await json('__gm.menuRows()')).includes('Project settings…'),
|
||||
JSON.stringify(await json('__gm.menuRows()')),
|
||||
);
|
||||
await clickText('Collision layers…');
|
||||
await clickText('Project settings…');
|
||||
const opened = await json('__gm.collisionLayers()');
|
||||
check(
|
||||
'the dialog opens on the one layer a project starts with',
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function pause(t) {
|
||||
has,
|
||||
shot,
|
||||
click,
|
||||
clickText,
|
||||
clickToolbar,
|
||||
press,
|
||||
entity,
|
||||
world,
|
||||
@@ -78,7 +78,7 @@ export async function pause(t) {
|
||||
|
||||
// 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.
|
||||
await clickText('❚❚ Pause');
|
||||
await clickToolbar('pause');
|
||||
const held = await playInfo();
|
||||
const heldX = await boxX();
|
||||
await sleep(1000);
|
||||
@@ -91,12 +91,14 @@ export async function pause(t) {
|
||||
Math.abs(stillHeld.time - held.time) < 0.05,
|
||||
`x ${heldX} → ${await boxX()}, clock ${held.time.toFixed(2)} → ${stillHeld.time.toFixed(2)} over 1 s`,
|
||||
);
|
||||
// `locate` throws on a missing label, so the button that must be *gone* is asked for through
|
||||
// the hook, which answers with null.
|
||||
// Since #183 the buttons carry no caption, so what they offer is read as the picture each one
|
||||
// wears: while the run is held, the pause button shows a second play sign, and the play button
|
||||
// has turned into the stop square for as long as the run is on.
|
||||
const heldButtons = await json('__gm.runButtons()');
|
||||
check(
|
||||
'the button offers to resume while it is held, and no longer to pause',
|
||||
(await has('▶ Resume')) === true && (await has('❚❚ Pause')) === false,
|
||||
`resume=${await has('▶ Resume')} pause=${await has('❚❚ Pause')}`,
|
||||
heldButtons.pause === 'play' && heldButtons.play === 'stop' && heldButtons.pauseEnabled === true,
|
||||
JSON.stringify(heldButtons),
|
||||
);
|
||||
await shot('36-paused');
|
||||
|
||||
@@ -261,7 +263,7 @@ export async function pause(t) {
|
||||
|
||||
await sleep(400);
|
||||
const runningClock = await clock();
|
||||
await clickText('❚❚ Pause');
|
||||
await clickToolbar('pause');
|
||||
const heldClock = await clock();
|
||||
await sleep(800);
|
||||
const stillHeldClock = await clock();
|
||||
|
||||
+28
-27
@@ -42,8 +42,9 @@ export async function loadScene(t) {
|
||||
evaluate,
|
||||
mutate,
|
||||
click,
|
||||
doubleClick,
|
||||
clickText,
|
||||
locate,
|
||||
clickToolbar,
|
||||
press,
|
||||
play,
|
||||
stop,
|
||||
@@ -53,10 +54,19 @@ export async function loadScene(t) {
|
||||
} = t;
|
||||
const playInfo = () => json('__gm.playInfo()');
|
||||
const playFrom = () => json('__gm.playFrom()');
|
||||
/** The switch is opened by position: its caption is elided at the width it has. */
|
||||
const openPlayFrom = async () => {
|
||||
const at = await json('__gm.playFromAt()');
|
||||
/**
|
||||
* The start-scene switch sits in the Project settings dialog since the top bar became icons
|
||||
* (#183): menu button → "Project settings…" → the switch → the mode → Apply.
|
||||
*/
|
||||
const setPlayFrom = async (mode) => {
|
||||
await clickToolbar('menu');
|
||||
await clickText('Project settings…');
|
||||
const at = await json('__gm.projectSettingsPlayFromAt()');
|
||||
await click(at.x, at.y);
|
||||
await clickText(mode);
|
||||
const chosen = await json('__gm.projectSettings()');
|
||||
await clickText('Apply');
|
||||
return chosen;
|
||||
};
|
||||
const stats = () => json('__gm.sound.stats()');
|
||||
const waitFor = async (read, done, tries = 25) => {
|
||||
@@ -120,9 +130,7 @@ export async function loadScene(t) {
|
||||
'with no start scene named, Play starts from the active one',
|
||||
(await json('__gm.state.project.startSceneId')) === '' &&
|
||||
(await playFrom()).mode === 'start' &&
|
||||
(await playFrom()).scene === firstName &&
|
||||
// The caption is the mode, worded like the menu entry it came from.
|
||||
(await playFrom()).label === 'Start scene',
|
||||
(await playFrom()).scene === firstName,
|
||||
JSON.stringify(await playFrom()),
|
||||
);
|
||||
|
||||
@@ -163,24 +171,18 @@ export async function loadScene(t) {
|
||||
JSON.stringify(started),
|
||||
);
|
||||
|
||||
// 35d². The scene selector keeps working while the run is on. It only moves the editor's view,
|
||||
// which a double-click on a scene in the Assets panel has always done – disabling the one
|
||||
// and not the other was the inconsistency. The run itself is untouched by it.
|
||||
const select = await json('__gm.sceneSelect()');
|
||||
await click(select.x, select.y);
|
||||
const rows = await json('__gm.menuRows()');
|
||||
// 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);
|
||||
await click(entry.x, entry.y);
|
||||
// 35d². Switching the scene the editor shows keeps working while the run is on. Since the top
|
||||
// bar lost its scene selector (#183) the Assets panel is the one way there, and it was
|
||||
// never blocked in play mode; the run itself is untouched by it.
|
||||
const sceneRow = await json('__gm.assetRow("ls-scene")');
|
||||
await doubleClick(sceneRow.x, sceneRow.y);
|
||||
const playing = await playInfo();
|
||||
check(
|
||||
'the scene selector stays usable in play mode, and the run runs on',
|
||||
select.enabled === true &&
|
||||
rows.some((row) => (row ?? '').includes('Level 2')) &&
|
||||
(await json('__gm.state.project.activeSceneId')) === 'ls-scene' &&
|
||||
'a scene opened from the Assets panel moves the editor in play mode, and the run runs on',
|
||||
(await json('__gm.state.project.activeSceneId')) === 'ls-scene' &&
|
||||
playing.playing === true &&
|
||||
playing.sceneId === 'ls-scene',
|
||||
JSON.stringify({ select, rows, playing }),
|
||||
JSON.stringify({ sceneRow, playing }),
|
||||
);
|
||||
|
||||
// 35e. The swap itself. The score was counted in the old scene; a fresh run would show 0.
|
||||
@@ -219,21 +221,20 @@ export async function loadScene(t) {
|
||||
|
||||
// 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.
|
||||
await openPlayFrom();
|
||||
await clickText('Active scene');
|
||||
const chosen = await setPlayFrom('Active scene');
|
||||
await play();
|
||||
const fromActive = await playInfo();
|
||||
check(
|
||||
'switched to "active scene", Play runs the scene the editor shows',
|
||||
fromActive.sceneId === firstId &&
|
||||
(await playFrom()).mode === 'active' &&
|
||||
(await playFrom()).label === 'Active scene' &&
|
||||
// The dialog held the new mode before Apply, and Apply left the start scene alone.
|
||||
chosen.playFrom === 'active' &&
|
||||
(await json('__gm.state.project.startSceneId')) === 'ls-scene',
|
||||
JSON.stringify({ fromActive, switch: await playFrom() }),
|
||||
JSON.stringify({ fromActive, chosen, switch: await playFrom() }),
|
||||
);
|
||||
await stop();
|
||||
await openPlayFrom();
|
||||
await clickText('Start scene');
|
||||
await setPlayFrom('Start scene');
|
||||
check(
|
||||
'the switch goes back to the start scene',
|
||||
(await playFrom()).mode === 'start' && (await playFrom()).scene === 'Level 2',
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* The top bar after its rework (#183): five icon buttons and no captions at all. What it used to
|
||||
* spell out moved into the one menu – the project list into "Open project…", the project name and
|
||||
* the start-scene switch into "Project settings…" next to the collision layers.
|
||||
*
|
||||
* Runs last, and on purpose: the project picker is opened here and left again with Cancel. Opening
|
||||
* or deleting a project for real would pull the project out from under every step before this one.
|
||||
*/
|
||||
export async function topbar(t) {
|
||||
const { check, json, has, shot, clickText, clickToolbar, typeText, press, sleep, waitUntil } = t;
|
||||
|
||||
await press('Escape');
|
||||
|
||||
// 46a. Nothing in the bar is written any more. The five words it used to carry are asked for as
|
||||
// drawn text: the buttons are still there, only their captions are gone.
|
||||
const captions = {
|
||||
project: await has('Project'),
|
||||
undo: await has('Undo'),
|
||||
redo: await has('Redo'),
|
||||
play: await has('Play'),
|
||||
pause: await has('Pause'),
|
||||
};
|
||||
check(
|
||||
'the top bar carries no captions any more – menu, undo, redo, play and pause are icons',
|
||||
Object.values(captions).every((shown) => shown === false),
|
||||
JSON.stringify(captions),
|
||||
);
|
||||
const buttons = await json('__gm.runButtons()');
|
||||
check(
|
||||
'in edit mode the run buttons offer play and nothing to pause',
|
||||
buttons.play === 'play' &&
|
||||
buttons.pause === 'pause' &&
|
||||
buttons.playEnabled === true &&
|
||||
buttons.pauseEnabled === false,
|
||||
JSON.stringify(buttons),
|
||||
);
|
||||
await shot('46-topbar');
|
||||
|
||||
// 46b. The one menu. Four entries, and the separator between what opens a project and what
|
||||
// changes the one that is open.
|
||||
await clickToolbar('menu');
|
||||
const rows = await json('__gm.menuRows()');
|
||||
check(
|
||||
'the menu button opens new project, the project picker and the project settings',
|
||||
JSON.stringify(rows) === JSON.stringify(['New project', 'Open project…', null, 'Project settings…']),
|
||||
JSON.stringify(rows),
|
||||
);
|
||||
|
||||
// 46c. The picker lists what the account owns, with the open project picked, and Cancel leaves
|
||||
// that project exactly where it was.
|
||||
await clickText('Open project…');
|
||||
// The picker fetches the list before it can open, so it is waited for rather than read at once.
|
||||
await waitUntil('__gm.projectPicker() !== null', { label: 'the project picker opened' });
|
||||
const picker = await json('__gm.projectPicker()');
|
||||
const projectId = await json('__gm.state.project.id');
|
||||
const projectName = await json('__gm.state.project.name');
|
||||
check(
|
||||
'"Open project…" lists the projects with the open one picked',
|
||||
picker !== null && picker.picked === projectId && picker.names.includes(projectName),
|
||||
JSON.stringify({ picker, projectId, projectName }),
|
||||
);
|
||||
await clickText('Cancel');
|
||||
check(
|
||||
'Cancel closes the picker and leaves the project loaded',
|
||||
(await json('__gm.projectPicker()')) === null && (await json('__gm.state.project.id')) === projectId,
|
||||
JSON.stringify({ picker: await json('__gm.projectPicker()'), id: await json('__gm.state.project.id') }),
|
||||
);
|
||||
|
||||
// 46d. The settings dialog holds the name, the start-scene switch and the collision layers of
|
||||
// #152 – the three things the bar itself used to carry.
|
||||
await clickToolbar('menu');
|
||||
await clickText('Project settings…');
|
||||
const settings = await json('__gm.projectSettings()');
|
||||
const layers = await json('__gm.collisionLayers()');
|
||||
check(
|
||||
'"Project settings…" opens on the project as it stands',
|
||||
settings?.name === projectName &&
|
||||
(settings.playFrom === 'start' || settings.playFrom === 'active') &&
|
||||
JSON.stringify(layers.dialog) === JSON.stringify(layers.project),
|
||||
JSON.stringify({ settings, layers }),
|
||||
);
|
||||
|
||||
// 46e. Renaming through it, and the icon buttons undoing and redoing it. The name is read off the
|
||||
// project rather than written down here: CI seeds the same project under a different name.
|
||||
const nameField = await t.locate(projectName);
|
||||
await t.doubleClick(nameField.x, nameField.y);
|
||||
await press('a', { ctrl: true });
|
||||
await typeText('Renamed by 46');
|
||||
await press('Enter');
|
||||
await clickText('Apply');
|
||||
await sleep(200);
|
||||
check(
|
||||
'Apply renames the project',
|
||||
(await json('__gm.state.project.name')) === 'Renamed by 46',
|
||||
await json('__gm.state.project.name'),
|
||||
);
|
||||
await clickToolbar('undo');
|
||||
await sleep(200);
|
||||
const undone = await json('__gm.state.project.name');
|
||||
await clickToolbar('redo');
|
||||
await sleep(200);
|
||||
const redone = await json('__gm.state.project.name');
|
||||
check(
|
||||
'the undo and redo buttons of the bar walk the rename back and forth',
|
||||
undone === projectName && redone === 'Renamed by 46',
|
||||
JSON.stringify({ undone, redone, was: projectName }),
|
||||
);
|
||||
// Back to the name the rest of the world knows it by.
|
||||
await clickToolbar('undo');
|
||||
await sleep(200);
|
||||
check(
|
||||
'the project is handed back under its own name',
|
||||
(await json('__gm.state.project.name')) === projectName,
|
||||
await json('__gm.state.project.name'),
|
||||
);
|
||||
await t.saved();
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import { shapes } from './42-shapes.mjs';
|
||||
import { queries } from './43-queries.mjs';
|
||||
import { documents } from './44-documents.mjs';
|
||||
import { login } from './45-login.mjs';
|
||||
import { topbar } from './46-topbar.mjs';
|
||||
|
||||
export const STEPS = [
|
||||
hierarchy,
|
||||
@@ -95,4 +96,5 @@ export const STEPS = [
|
||||
queries,
|
||||
documents,
|
||||
login,
|
||||
topbar,
|
||||
];
|
||||
Reference in new issue
Block a user