game-harness 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes are recorded here. Releases follow
4
4
  [Semantic Versioning](https://semver.org/) and are generated from Conventional
5
5
  Commits by release-please.
6
6
 
7
+ ## [1.3.0](https://github.com/jbcom/game-harness/compare/game-harness-v1.2.0...game-harness-v1.3.0) (2026-10-10)
8
+
9
+
10
+ ### Features
11
+
12
+ * **chromium:** add the macos-hardware-metal gpu mode ([008f277](https://github.com/jbcom/game-harness/commit/008f2770886e73a2d323b65bb3f6373ea21be426))
13
+
14
+ ## [1.2.0](https://github.com/jbcom/game-harness/compare/game-harness-v1.1.2...game-harness-v1.2.0) (2026-10-10)
15
+
16
+
17
+ ### Features
18
+
19
+ * **vitest:** pass the browser server's api, custom commands, failure screenshots and isolation through ([29c6d2d](https://github.com/jbcom/game-harness/commit/29c6d2dbaf099b7dcdf8a9cd2b1ffa621e9d9059))
20
+
7
21
  ## [1.1.2](https://github.com/jbcom/game-harness/compare/game-harness-v1.1.1...game-harness-v1.1.2) (2026-10-07)
8
22
 
9
23
 
package/README.md CHANGED
@@ -180,6 +180,11 @@ make WebGL start. The default `auto` profile lets Chromium select the native
180
180
  renderer (including Metal on macOS). `software` is an explicit SwiftShader
181
181
  fallback. `linux-hardware-vulkan` applies the reviewed Mesa/ANGLE flags and
182
182
  `EGL_PLATFORM=surfaceless` for a runner that exposes `/dev/dri/renderD128`.
183
+ `macos-hardware-metal` asks ANGLE for its Metal backend (`--use-angle=metal
184
+ --ignore-gpu-blocklist`); it is the profile for an Apple Silicon Mac with no
185
+ window session, where `headless: true` still reaches the real GPU (no display
186
+ server or Xvfb is involved), so a render host does not fall back to
187
+ SwiftShader.
183
188
 
184
189
  Vitest's interactive UI is disabled by default even though the Chromium window
185
190
  remains visible. This gives Playwright a fixed viewport and a deterministic
@@ -252,11 +257,12 @@ const browser = await chromium.launch({ args, env, headless: false });
252
257
  `createChromiumLaunchProfile()` is the peer-free primitive behind both
253
258
  `definePlaywrightConfig()` and `defineBrowserTestConfig()` — use it directly
254
259
  when driving Chromium yourself (a custom launch script, `production-runtime`'s
255
- own internals, or a non-Playwright automation layer). It resolves one of three
260
+ own internals, or a non-Playwright automation layer). It resolves one of four
256
261
  renderer policies (`auto` leaves selection to Chromium; `software` opts into
257
262
  SwiftShader explicitly; `linux-hardware-vulkan` applies the reviewed
258
263
  Mesa/ANGLE flags plus `EGL_PLATFORM=surfaceless` for a runner exposing
259
- `/dev/dri/renderD128`) and always de-duplicates and appends `--mute-audio`
264
+ `/dev/dri/renderD128`; `macos-hardware-metal` asks ANGLE for its Metal backend
265
+ with `--use-angle=metal --ignore-gpu-blocklist`) and always de-duplicates and appends `--mute-audio`
260
266
  last, so a caller-supplied arg list can never accidentally drop the silence
261
267
  guard. Every profile also carries `CHROMIUM_ANTI_THROTTLING_ARGS`
262
268
  (`--disable-background-timer-throttling`, `--disable-renderer-backgrounding`,
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defineBrowserTestConfig = defineBrowserTestConfig;
4
4
  const browser_playwright_1 = require("@vitest/browser-playwright");
5
5
  const chromium_launch_js_1 = require("./chromium-launch.js");
6
+ const vitest_major_js_1 = require("./vitest-major.js");
6
7
  function resolveHeadless(headless) {
7
8
  if (typeof headless === 'boolean')
8
9
  return headless;
@@ -40,7 +41,7 @@ function resolveHeadless(headless) {
40
41
  * one, wrap the test command in `xvfb-run` rather than forcing headless here.
41
42
  */
42
43
  function defineBrowserTestConfig(opts = {}) {
43
- const { contextOptions = {}, gpuArgs = [], gpuMode = 'auto', headless = false, ui = false, instances = [{ browser: 'chromium' }], optimizeDeps = [], setupFiles, name = 'browser', include = ['tests/browser/**/*.browser.test.{ts,tsx}'], fileParallelism = false, } = opts;
44
+ const { contextOptions = {}, gpuArgs = [], gpuMode = 'auto', headless = false, ui = false, instances = [{ browser: 'chromium' }], optimizeDeps = [], setupFiles, name = 'browser', include = ['tests/browser/**/*.browser.test.{ts,tsx}'], fileParallelism = false, api, commands, screenshotFailures, isolate, } = opts;
44
45
  if (!name.trim())
45
46
  throw new TypeError('browser project name must not be empty');
46
47
  if (instances.length === 0) {
@@ -73,6 +74,19 @@ function defineBrowserTestConfig(opts = {}) {
73
74
  if (setupFiles) {
74
75
  test.setupFiles = [...setupFiles];
75
76
  }
77
+ if (api) {
78
+ if ((0, vitest_major_js_1.vitestMajor)() >= 5)
79
+ test.api = { ...api };
80
+ // Vitest 4's browser server reads its own `api`; Vitest 5's types no longer carry it.
81
+ else
82
+ test.browser.api = { ...api };
83
+ }
84
+ if (commands)
85
+ test.browser = { ...test.browser, commands: { ...commands } };
86
+ if (screenshotFailures !== undefined)
87
+ test.browser = { ...test.browser, screenshotFailures };
88
+ if (isolate !== undefined)
89
+ test.isolate = isolate;
76
90
  if (optimizeDeps.length > 0) {
77
91
  // Vitest's `test` fragment has no `optimizeDeps` field (that lives at
78
92
  // the top-level Vite config) — surface the caller's list here so a
@@ -11,6 +11,7 @@ const GPU_ARGS = {
11
11
  '--use-angle=vulkan',
12
12
  '--ignore-gpu-blocklist',
13
13
  ],
14
+ 'macos-hardware-metal': ['--use-angle=metal', '--ignore-gpu-blocklist'],
14
15
  };
15
16
  /**
16
17
  * Keeps timers and rendering on schedule in pages Chromium considers
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.vitestMajor = vitestMajor;
4
+ const node_1 = require("vitest/node");
5
+ /**
6
+ * The major version of the Vitest the consumer installed. The peer range spans
7
+ * Vitest 4 and 5, which disagree about where some browser-mode options live
8
+ * (Vitest 5 moved the browser server's port from `browser.api` to the
9
+ * top-level `api`, and ignores the old one).
10
+ */
11
+ function vitestMajor() {
12
+ const major = Number.parseInt(node_1.version, 10);
13
+ if (!Number.isFinite(major))
14
+ throw new Error(`unrecognised Vitest version: ${node_1.version}`);
15
+ return major;
16
+ }
@@ -1,5 +1,6 @@
1
1
  import { playwright } from '@vitest/browser-playwright';
2
2
  import { createChromiumLaunchProfile } from './chromium-launch.js';
3
+ import { vitestMajor } from './vitest-major.js';
3
4
  function resolveHeadless(headless) {
4
5
  if (typeof headless === 'boolean')
5
6
  return headless;
@@ -37,7 +38,7 @@ function resolveHeadless(headless) {
37
38
  * one, wrap the test command in `xvfb-run` rather than forcing headless here.
38
39
  */
39
40
  export function defineBrowserTestConfig(opts = {}) {
40
- const { contextOptions = {}, gpuArgs = [], gpuMode = 'auto', headless = false, ui = false, instances = [{ browser: 'chromium' }], optimizeDeps = [], setupFiles, name = 'browser', include = ['tests/browser/**/*.browser.test.{ts,tsx}'], fileParallelism = false, } = opts;
41
+ const { contextOptions = {}, gpuArgs = [], gpuMode = 'auto', headless = false, ui = false, instances = [{ browser: 'chromium' }], optimizeDeps = [], setupFiles, name = 'browser', include = ['tests/browser/**/*.browser.test.{ts,tsx}'], fileParallelism = false, api, commands, screenshotFailures, isolate, } = opts;
41
42
  if (!name.trim())
42
43
  throw new TypeError('browser project name must not be empty');
43
44
  if (instances.length === 0) {
@@ -70,6 +71,19 @@ export function defineBrowserTestConfig(opts = {}) {
70
71
  if (setupFiles) {
71
72
  test.setupFiles = [...setupFiles];
72
73
  }
74
+ if (api) {
75
+ if (vitestMajor() >= 5)
76
+ test.api = { ...api };
77
+ // Vitest 4's browser server reads its own `api`; Vitest 5's types no longer carry it.
78
+ else
79
+ test.browser.api = { ...api };
80
+ }
81
+ if (commands)
82
+ test.browser = { ...test.browser, commands: { ...commands } };
83
+ if (screenshotFailures !== undefined)
84
+ test.browser = { ...test.browser, screenshotFailures };
85
+ if (isolate !== undefined)
86
+ test.isolate = isolate;
73
87
  if (optimizeDeps.length > 0) {
74
88
  // Vitest's `test` fragment has no `optimizeDeps` field (that lives at
75
89
  // the top-level Vite config) — surface the caller's list here so a
@@ -7,6 +7,7 @@ const GPU_ARGS = {
7
7
  '--use-angle=vulkan',
8
8
  '--ignore-gpu-blocklist',
9
9
  ],
10
+ 'macos-hardware-metal': ['--use-angle=metal', '--ignore-gpu-blocklist'],
10
11
  };
11
12
  /**
12
13
  * Keeps timers and rendering on schedule in pages Chromium considers
@@ -0,0 +1,13 @@
1
+ import { version } from 'vitest/node';
2
+ /**
3
+ * The major version of the Vitest the consumer installed. The peer range spans
4
+ * Vitest 4 and 5, which disagree about where some browser-mode options live
5
+ * (Vitest 5 moved the browser server's port from `browser.api` to the
6
+ * top-level `api`, and ignores the old one).
7
+ */
8
+ export function vitestMajor() {
9
+ const major = Number.parseInt(version, 10);
10
+ if (!Number.isFinite(major))
11
+ throw new Error(`unrecognised Vitest version: ${version}`);
12
+ return major;
13
+ }
@@ -1,5 +1,5 @@
1
1
  import { type PlaywrightProviderOptions } from '@vitest/browser-playwright';
2
- import type { TestUserConfig } from 'vitest/node';
2
+ import type { BrowserConfigOptions, TestUserConfig } from 'vitest/node';
3
3
  import { type ChromiumGpuMode } from './chromium-launch.js';
4
4
  /**
5
5
  * A single browser instance entry, as accepted by Vitest Browser Mode's
@@ -64,6 +64,25 @@ export interface BrowserTestConfigOptions {
64
64
  * verified your suite tolerates concurrent browser contexts.
65
65
  */
66
66
  fileParallelism?: boolean;
67
+ /**
68
+ * The browser server's address, such as the port a CI slot or a local
69
+ * port allocator assigned (`{ port, strictPort: true }`). Placed where the
70
+ * installed Vitest reads it: the top-level `api` on Vitest 5, `browser.api`
71
+ * on Vitest 4. Omitted, Vitest's default port is used.
72
+ */
73
+ api?: BrowserApiConfig;
74
+ /** Custom browser commands (`browser.commands`), e.g. a pointer drawn along a path. */
75
+ commands?: NonNullable<BrowserConfigOptions['commands']>;
76
+ /** `browser.screenshotFailures`. Omitted, Vitest's default applies. */
77
+ screenshotFailures?: boolean;
78
+ /** The top-level `isolate` (Vitest 5 retired `browser.isolate` for it). Omitted, Vitest's default applies. */
79
+ isolate?: boolean;
80
+ }
81
+ /** The browser server's address options: the subset Vitest 4's `browser.api` and Vitest 5's `api` share. */
82
+ export interface BrowserApiConfig {
83
+ port?: number;
84
+ strictPort?: boolean;
85
+ host?: string;
67
86
  }
68
87
  /**
69
88
  * Builds a `test` fragment for a Vitest Browser Mode project, wired for
@@ -1,5 +1,5 @@
1
1
  import { type PlaywrightProviderOptions } from '@vitest/browser-playwright';
2
- import type { TestUserConfig } from 'vitest/node';
2
+ import type { BrowserConfigOptions, TestUserConfig } from 'vitest/node';
3
3
  import { type ChromiumGpuMode } from './chromium-launch.js';
4
4
  /**
5
5
  * A single browser instance entry, as accepted by Vitest Browser Mode's
@@ -64,6 +64,25 @@ export interface BrowserTestConfigOptions {
64
64
  * verified your suite tolerates concurrent browser contexts.
65
65
  */
66
66
  fileParallelism?: boolean;
67
+ /**
68
+ * The browser server's address, such as the port a CI slot or a local
69
+ * port allocator assigned (`{ port, strictPort: true }`). Placed where the
70
+ * installed Vitest reads it: the top-level `api` on Vitest 5, `browser.api`
71
+ * on Vitest 4. Omitted, Vitest's default port is used.
72
+ */
73
+ api?: BrowserApiConfig;
74
+ /** Custom browser commands (`browser.commands`), e.g. a pointer drawn along a path. */
75
+ commands?: NonNullable<BrowserConfigOptions['commands']>;
76
+ /** `browser.screenshotFailures`. Omitted, Vitest's default applies. */
77
+ screenshotFailures?: boolean;
78
+ /** The top-level `isolate` (Vitest 5 retired `browser.isolate` for it). Omitted, Vitest's default applies. */
79
+ isolate?: boolean;
80
+ }
81
+ /** The browser server's address options: the subset Vitest 4's `browser.api` and Vitest 5's `api` share. */
82
+ export interface BrowserApiConfig {
83
+ port?: number;
84
+ strictPort?: boolean;
85
+ host?: string;
67
86
  }
68
87
  /**
69
88
  * Builds a `test` fragment for a Vitest Browser Mode project, wired for
@@ -1,12 +1,16 @@
1
1
  /** Browser renderer policy for agent-controlled Chromium sessions. */
2
- export type ChromiumGpuMode = 'auto' | 'software' | 'linux-hardware-vulkan';
2
+ export type ChromiumGpuMode = 'auto' | 'software' | 'linux-hardware-vulkan' | 'macos-hardware-metal';
3
3
  export type ChromiumEnvironment = Record<string, string | undefined>;
4
4
  export interface ChromiumLaunchProfileOptions {
5
5
  /**
6
- * `auto` leaves renderer selection to Chromium (Metal on macOS, the native
7
- * desktop stack elsewhere). `software` opts into SwiftShader explicitly.
6
+ * `auto` leaves renderer selection to Chromium (Metal on macOS in a headed
7
+ * window, the native desktop stack elsewhere). `software` opts into
8
+ * SwiftShader explicitly.
8
9
  * `linux-hardware-vulkan` is a proven Mesa/ANGLE profile for a
9
10
  * Linux runner with `/dev/dri/renderD128` passed through.
11
+ * `macos-hardware-metal` asks ANGLE for its Metal backend, which gives a
12
+ * headless Chromium on an Apple Silicon Mac (no window session needed) the
13
+ * real GPU instead of SwiftShader.
10
14
  */
11
15
  gpuMode?: ChromiumGpuMode;
12
16
  /** Additional Chromium arguments, de-duplicated ahead of `--mute-audio`. */
@@ -1,12 +1,16 @@
1
1
  /** Browser renderer policy for agent-controlled Chromium sessions. */
2
- export type ChromiumGpuMode = 'auto' | 'software' | 'linux-hardware-vulkan';
2
+ export type ChromiumGpuMode = 'auto' | 'software' | 'linux-hardware-vulkan' | 'macos-hardware-metal';
3
3
  export type ChromiumEnvironment = Record<string, string | undefined>;
4
4
  export interface ChromiumLaunchProfileOptions {
5
5
  /**
6
- * `auto` leaves renderer selection to Chromium (Metal on macOS, the native
7
- * desktop stack elsewhere). `software` opts into SwiftShader explicitly.
6
+ * `auto` leaves renderer selection to Chromium (Metal on macOS in a headed
7
+ * window, the native desktop stack elsewhere). `software` opts into
8
+ * SwiftShader explicitly.
8
9
  * `linux-hardware-vulkan` is a proven Mesa/ANGLE profile for a
9
10
  * Linux runner with `/dev/dri/renderD128` passed through.
11
+ * `macos-hardware-metal` asks ANGLE for its Metal backend, which gives a
12
+ * headless Chromium on an Apple Silicon Mac (no window session needed) the
13
+ * real GPU instead of SwiftShader.
10
14
  */
11
15
  gpuMode?: ChromiumGpuMode;
12
16
  /** Additional Chromium arguments, de-duplicated ahead of `--mute-audio`. */
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The major version of the Vitest the consumer installed. The peer range spans
3
+ * Vitest 4 and 5, which disagree about where some browser-mode options live
4
+ * (Vitest 5 moved the browser server's port from `browser.api` to the
5
+ * top-level `api`, and ignores the old one).
6
+ */
7
+ export declare function vitestMajor(): number;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The major version of the Vitest the consumer installed. The peer range spans
3
+ * Vitest 4 and 5, which disagree about where some browser-mode options live
4
+ * (Vitest 5 moved the browser server's port from `browser.api` to the
5
+ * top-level `api`, and ignores the old one).
6
+ */
7
+ export declare function vitestMajor(): number;
@@ -17,10 +17,13 @@ const browser = await chromium.launch({ args, env, headless: false });
17
17
  `definePlaywrightConfig()` and `defineBrowserTestConfig()` — use it directly
18
18
  when driving Chromium yourself (a custom launch script, `production-runtime`'s
19
19
  own internals, or a non-Playwright automation layer). It resolves one of
20
- three renderer policies (`auto` leaves selection to Chromium; `software`
20
+ four renderer policies (`auto` leaves selection to Chromium; `software`
21
21
  opts into SwiftShader explicitly; `linux-hardware-vulkan` applies the
22
22
  reviewed Mesa/ANGLE flags plus `EGL_PLATFORM=surfaceless` for a runner
23
- exposing `/dev/dri/renderD128`) and always de-duplicates and appends
23
+ exposing `/dev/dri/renderD128`; `macos-hardware-metal` asks ANGLE for its
24
+ Metal backend with `--use-angle=metal --ignore-gpu-blocklist`, which a
25
+ headless Chromium on an Apple Silicon Mac with no window session honours by
26
+ using the real GPU) and always de-duplicates and appends
24
27
  `--mute-audio` last, so a caller-supplied arg list can never accidentally
25
28
  drop the silence guard.
26
29
 
@@ -43,7 +43,11 @@ to make WebGL start. The default `auto` profile lets Chromium select the
43
43
  native renderer (including Metal on macOS). `software` is an explicit
44
44
  SwiftShader fallback. `linux-hardware-vulkan` applies the reviewed
45
45
  Mesa/ANGLE flags and `EGL_PLATFORM=surfaceless` for a runner that exposes
46
- `/dev/dri/renderD128`.
46
+ `/dev/dri/renderD128`. `macos-hardware-metal` asks ANGLE for its Metal backend
47
+ (`--use-angle=metal --ignore-gpu-blocklist`); it is the profile for an Apple
48
+ Silicon Mac with no window session, where `headless: true` still reaches the
49
+ real GPU (no display server or Xvfb is involved), so a render host does not
50
+ fall back to SwiftShader.
47
51
 
48
52
  A Gitea runner job using the hardware profile must install
49
53
  `mesa-vulkan-drivers` and `xvfb`, require the render device with
@@ -39,3 +39,18 @@ discover on its own) that must also be merged into your top-level
39
39
  `vite.config.ts`'s `optimizeDeps.include` — read them off the returned
40
40
  fragment's `__optimizeDepsInclude` field, since Vitest's `test` block has no
41
41
  `optimizeDeps` field of its own.
42
+
43
+ A game whose suite needs more of Vitest's browser options passes them through
44
+ rather than spreading over the fragment:
45
+
46
+ - `api` is the browser server's address, such as the port a CI slot or a
47
+ local port allocator assigned, with `strictPort: true` so a taken port
48
+ fails rather than drifting. It is placed where the installed Vitest reads
49
+ it: the top-level `api` on Vitest 5 (which ignores `browser.api`),
50
+ `browser.api` on Vitest 4.
51
+ - `commands` registers custom browser commands (`browser.commands`), such as
52
+ a pointer drawn along a path that `userEvent` cannot draw.
53
+ - `screenshotFailures` sets `browser.screenshotFailures`.
54
+ - `isolate` sets the top-level `isolate`, which replaced `browser.isolate`.
55
+
56
+ Each is left to Vitest's default when it is not given.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "game-harness",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Release-grade browser QA primitives for TypeScript games: silent Playwright/Vitest sessions, deterministic screenshots, runtime proof, and Lighthouse gates.",
5
5
  "type": "module",
6
6
  "license": "MIT",