Show only the game view in fullscreen, in the editor and the link (#267)
game-maker / gate (push) Successful in 4m10s
game-maker / suite (push) Failing after 11m53s

A game is easier to judge and to play without the editor around it.
The Game tab and the game link now both have a button that puts the
game view itself into fullscreen, not just the browser window.

- The Game tab header has a fullscreen button on the right. It hides
  toolbar, panels, tab headers and console and keeps the zoom from the
  picker. A running game keeps running.
- A corner button at the top right, or Esc, leaves fullscreen and puts
  the view back into the tab where it was.
- The game link shows the same corner button. Over a running game it
  hides after 2 s without mouse movement and returns on the next move.
- A click on the corner button never reaches the game.
- Stop ends fullscreen. Stop switches back to the Scene tab, and any
  switch away from the Game tab ends fullscreen, so no empty screen is
  left behind.
- src/viewport/fullscreen.ts wraps the Fullscreen API and holds the
  rule for when the button shows. FullscreenCorner.ts is the button
  used by both pages. Two SVG icons are new.
- Editor.showGameFullscreen moves the GameViewport into the root while
  fullscreen is on.

Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (3 new unit tests).
- e2e 60: 2 new checks for the corner button on the game link.
- e2e 61 (new): 4 checks for fullscreen in the editor.
- Full e2e run (local): ALL PASSED, 1580 checks.
- Counter-checks fail as expected: rule without "no game running",
  no stopPropagation (60h), no end on tab switch (61c), no reparenting
  of the view (61a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-29 14:44:14 +02:00
1 parent 24d360aeec
commit cab1c0177f
17 files changed
+356 -8

No files matched your search

+36 -2
View File
@@ -77,8 +77,8 @@ function game(id) {
/**
* Releasing a project (#266): published from the main menu, played from the link without a
* session – only the game, zoomed to Auto, on the page colour – updated under the same link, and
* taken down.
* session – only the game, zoomed to Auto, on the page colour, with a fullscreen button in the
* corner (#267) – updated under the same link, and taken down.
*
* Works on a throwaway project of its own, like step 50. The last thing it does is delete that
* project and open the fixture again.
@@ -230,6 +230,7 @@ export async function release(t) {
await click(size.w / 2, size.h / 2);
await waitUntil('__gameMakerPlay.running()', { label: 'the game runs' });
await sleep(300);
const cornerAfterClick = await player('fullscreen()');
const screen = await player('screen()');
const centre = { x: screen.frame.x + screen.frame.w / 2, y: screen.frame.y + screen.frame.h / 2 };
const scale = screen.scale;
@@ -256,6 +257,23 @@ export async function release(t) {
playingCursor,
);
// 60g (#267). The fullscreen button sits in the top right corner while the mouse moves, steps
// aside once it rests over the running game, and is back with the next move.
await sleep(2300);
const rested = await player('fullscreen()');
await t.mouse('mouseMoved', size.w / 2 + 30, size.h / 2 + 30);
await settle();
const movedAgain = await player('fullscreen()');
check(
'over a running game the fullscreen button shows while the mouse moves and steps aside when it rests',
cornerAfterClick.button !== null &&
cornerAfterClick.button.x > size.w - 60 &&
cornerAfterClick.button.y < 60 &&
rested.button === null &&
movedAgain.button !== null,
JSON.stringify({ cornerAfterClick, rested, movedAgain }),
);
// 60e. The keys go to the game: the arrow moves the crate out of the middle, S stops the game.
await holdKey('ArrowRight', 500);
await sleep(200);
@@ -267,6 +285,22 @@ export async function release(t) {
moved === BACKGROUND && (await player('screen()')).caption === 'Click to play again',
JSON.stringify({ moved, screen: await player('screen()') }),
);
// 60h (#267). A click on the corner button makes the page fullscreen and does not reach the
// game – it would start it again; a second click leaves fullscreen.
const corner = (await player('fullscreen()')).button;
if (corner) await click(corner.x, corner.y);
await sleep(500);
const entered = { ...(await player('fullscreen()')), running: await player('running()') };
const exitAt = entered.button ?? corner;
if (exitAt) await click(exitAt.x, exitAt.y);
await sleep(500);
const left = await player('fullscreen()');
check(
'the corner button puts the page into fullscreen and back, without starting the game',
corner !== null && entered.on && !entered.running && !left.on,
JSON.stringify({ corner, entered, left }),
);
check('the play page threw nothing', exceptions().length === 0, exceptions().join(' | '));
// 60f. Take down asks, then the link is dead – for the API and for the page.
+64
View File
@@ -0,0 +1,64 @@
/**
* Fullscreen of the game tab (#267): the button in its header puts the game view on its own into
* fullscreen – no toolbar, no panels, no header –, the game keeps running, and the button in the
* corner puts the view back under its header. Stopping the game from fullscreen ends it as well.
*/
export async function fullscreen(t) {
const { check, json, click, sleep, play, stop, shot, press, exceptions } = t;
const info = async () => ({
...(await json('__gm.gameFullscreen()')),
view: (await json('__gm.gameScale()')).view,
});
const size = { w: await json('innerWidth'), h: await json('innerHeight') };
await press('Escape');
await play();
const windowed = await info();
// 61a. The header button: the game view alone fills the page, and the game goes on.
await click(windowed.headerButton.x, windowed.headerButton.y);
await sleep(500);
const full = { ...(await info()), mode: await json('__gm.state.mode') };
check(
'the header button puts the running game view alone into fullscreen',
windowed.view.y > 0 &&
full.on &&
full.document &&
full.headerButton === null &&
full.view.x === 0 &&
full.view.y === 0 &&
full.view.w === size.w &&
full.view.h === size.h &&
full.mode === 'play',
JSON.stringify({ size, windowed, full }),
);
await shot('61-fullscreen');
// 61b. The corner button leaves it, and the view is back under its header, as big as before.
if (full.corner) await click(full.corner.x, full.corner.y);
await sleep(500);
const back = await info();
check(
'the corner button leaves fullscreen and puts the view back in its tab',
full.corner !== null &&
!back.on &&
!back.document &&
back.headerButton !== null &&
JSON.stringify(back.view) === JSON.stringify(windowed.view),
JSON.stringify({ corner: full.corner, back }),
);
// 61c. Stop goes back to the scene tab, which ends fullscreen too.
await click(back.headerButton.x, back.headerButton.y);
await sleep(500);
const again = await info();
await stop();
await sleep(300);
const stopped = await info();
check(
'stopping the game from fullscreen ends fullscreen',
again.on && !stopped.on && !stopped.document,
JSON.stringify({ again: again.on, stopped }),
);
check('fullscreen threw nothing', exceptions().length === 0, exceptions().join(' | '));
}
+2
View File
@@ -63,6 +63,7 @@ import { objectReferences } from './57-object-references.mjs';
import { clipAreas } from './58-clip-areas.mjs';
import { whilePlaying } from './59-while-playing.mjs';
import { release } from './60-release.mjs';
import { fullscreen } from './61-fullscreen.mjs';
export const STEPS = [
hierarchy,
@@ -125,4 +126,5 @@ export const STEPS = [
clipAreas,
whilePlaying,
release,
fullscreen,
];