game-harness 1.1.1 → 1.2.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/AGENTS.md CHANGED
@@ -33,7 +33,8 @@ pnpm exec playwright install chromium
33
33
  pnpm add -D game-harness
34
34
  ```
35
35
 
36
- Node 22 or newer is required.
36
+ Node.js 22, 24 and 26 are supported (`engines.node: >=22`). This maintained-line
37
+ policy does not require equality to a patch release.
37
38
 
38
39
  ### Entry points
39
40
 
@@ -105,8 +106,9 @@ pnpm install --frozen-lockfile
105
106
  pnpm verify
106
107
  ```
107
108
 
108
- `mise.toml` is local-only. CI reads the Node version from `.nvmrc` and the
109
- pnpm version from `package.json`'s `packageManager` field via the official
109
+ `mise.toml` is local-only. CI verifies Node majors 22, 24 and 26; `.nvmrc`
110
+ selects major 26 for local development. Release and docs jobs select `lts/*`.
111
+ CI reads the pnpm version from `package.json`'s `packageManager` field via the official
110
112
  `actions/setup-node` and `pnpm/action-setup` actions — never edit a
111
113
  hardcoded version string into a workflow file.
112
114
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ 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.2.0](https://github.com/jbcom/game-harness/compare/game-harness-v1.1.2...game-harness-v1.2.0) (2026-10-10)
8
+
9
+
10
+ ### Features
11
+
12
+ * **vitest:** pass the browser server's api, custom commands, failure screenshots and isolation through ([29c6d2d](https://github.com/jbcom/game-harness/commit/29c6d2dbaf099b7dcdf8a9cd2b1ffa621e9d9059))
13
+
14
+ ## [1.1.2](https://github.com/jbcom/game-harness/compare/game-harness-v1.1.1...game-harness-v1.1.2) (2026-10-07)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * declare maintained Node support and standardize CI gates ([86b5dae](https://github.com/jbcom/game-harness/commit/86b5daea84d5680b2a1252e4f4d3dfbccb2754db))
20
+ * declare the supported Node lines and test each in CI ([3454eb6](https://github.com/jbcom/game-harness/commit/3454eb6b13c55593ff12eba1f4ff78edaf51f8f6))
21
+
7
22
  ## [1.1.1](https://github.com/jbcom/game-harness/compare/game-harness-v1.1.0...game-harness-v1.1.1) (2026-10-07)
8
23
 
9
24
 
package/README.md CHANGED
@@ -41,7 +41,7 @@ and Vitest Browser Mode.
41
41
  `@jbdevprimary/game-harness` package is a personal-user scope and is being
42
42
  retired; update existing dependency declarations and imports to `game-harness`.
43
43
 
44
- Node 22 or newer is required. Install the package plus only the peer family for
44
+ Node.js 22, 24 and 26 are supported. Install the package plus only the peer family for
45
45
  the integration you use:
46
46
 
47
47
  ```sh
@@ -123,11 +123,11 @@ framework entry point it imports. A Playwright-only game, for example, installs
123
123
 
124
124
  ## Current release matrix
125
125
 
126
- `engines.node` declares `>=22`, the earliest Node LTS line still actively
127
- supported. CI pins the primary gate to the version in `.nvmrc` (currently
128
- 24.19.0, the latest Node 24 LTS patch) and additionally runs the full test
129
- and build suite against Node 22 on Linux to prove the floor of that range,
130
- alongside macOS and Windows portability on the pinned version. The current
126
+ `engines.node` declares `>=22`. Supported maintained lines are Node.js 22,
127
+ 24 and 26; CI runs the full verification and packed-consumer smoke on each
128
+ line on Linux, plus Node 26 portability on macOS and Windows. `.nvmrc`
129
+ selects major 26 without requiring an exact patch. This is a maintained-line
130
+ policy, not a promise about every historical patch. The current
131
131
  conformance matrix is Playwright 1.62.1 and Vitest Browser 4.1.10 and 5.0.3.
132
132
  Package-boundary consumers run with a credential-free home directory and npm
133
133
  configuration, install only the peer family needed by each entry point, and
@@ -313,7 +313,7 @@ Keep `reuseExistingServer` false for release evidence and assert the game
313
313
  identity before exercising a journey. A process from another repository on a
314
314
  familiar port must never be accepted as proof.
315
315
 
316
- Before publishing, run `pnpm verify` under the pinned release toolchain. The
316
+ Before publishing, run `pnpm verify` under a supported Node line. The
317
317
  package verifier uses npm 11.17.0 directly from the package directory, packs a
318
318
  tarball, and installs it into credential-free temporary consumers for the
319
319
  peer-free root, Playwright/production-runtime, and Vitest Browser boundaries.
@@ -526,7 +526,9 @@ An agent integrating this package should read [AGENTS.md](AGENTS.md) first;
526
526
 
527
527
  ## Development
528
528
 
529
- Use the pinned Node and pnpm versions so the local gate matches CI. With
529
+ Use Node.js 22, 24 or 26, npm 11 for packed-consumer verification, and the
530
+ package's declared pnpm version. On Node 22, install npm 11 with
531
+ `npm install --global npm@11` to avoid npm 10's optional-peer resolver crash. With
530
532
  [mise](https://mise.jdx.dev) (recommended — it also reads `.nvmrc` and keeps
531
533
  pnpm current via `mise.toml`):
532
534
 
@@ -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
@@ -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
@@ -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
@@ -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;
@@ -3,7 +3,10 @@ title: Contributing
3
3
  description: Development, validation, and pull-request expectations.
4
4
  ---
5
5
 
6
- Game Harness uses pnpm and Node from `.nvmrc` (Node 22 or newer).
6
+ Game Harness uses pnpm and supports Node.js 22, 24 and 26. `.nvmrc` selects
7
+ major 26 for local development; no exact patch is required.
8
+ Use npm 11 for packed-consumer verification (`npm install --global npm@11`);
9
+ Node 22's bundled npm 10 has an optional-peer resolver crash in that smoke test.
7
10
 
8
11
  ```sh
9
12
  mise install # or: nvm use && corepack enable
package/docs/decisions.md CHANGED
@@ -6,6 +6,20 @@ description: Why the package is shaped the way it is, and how a predecessor harn
6
6
  Each entry records a decision, the reason for it, and what it means for a
7
7
  consumer. Newest first.
8
8
 
9
+ ## Maintained Node lines
10
+
11
+ Keep `engines.node: >=22`: Node.js 22, 24 and 26 are the supported maintained
12
+ lines. Linux CI runs full verification and packed-consumer smoke on all three;
13
+ local verification covers Node 22 and 26. `.nvmrc` selects major 26, and release
14
+ and documentation jobs use `lts/*`. No hook or script requires an exact patch.
15
+ This policy covers maintained lines rather than every historical patch, and
16
+ does not change runtime behavior or the public API.
17
+
18
+ Packed-consumer verification uses npm 11 on every CI verification leg. The
19
+ Node 22 bundled npm 10.9.9 crashes in its optional-peer resolver (`edgesOut`)
20
+ when installing the Vitest consumer; npm 11 handles the same tarball and peer
21
+ set. This is verification tooling, not an additional runtime requirement.
22
+
9
23
  ## Pixel tolerance for screenshot baselines
10
24
 
11
25
  Byte equality is not stable across renders, even on the same machine. Decode
@@ -3,7 +3,7 @@ title: Getting started
3
3
  description: Install Game Harness and pick the entry points your game needs.
4
4
  ---
5
5
 
6
- Node 22 or newer is required. Install the package plus only the peer family
6
+ Node.js 22, 24 and 26 are supported. Install the package plus only the peer family
7
7
  for the integration you use:
8
8
 
9
9
  ```sh
@@ -26,11 +26,11 @@ the framework entry point it imports — a Playwright-only game installs
26
26
 
27
27
  ## Current release matrix
28
28
 
29
- `engines.node` declares `>=22`, the earliest Node LTS line still actively
30
- supported. CI pins the primary gate to the version in `.nvmrc` (currently
31
- 24.19.0, the latest Node 24 LTS patch) and additionally runs the full test
32
- and build suite against Node 22 on Linux to prove the floor of that range,
33
- alongside macOS and Windows portability on the pinned version. The current
29
+ `engines.node` declares `>=22`. Supported maintained lines are Node.js 22,
30
+ 24 and 26; CI runs the full verification and packed-consumer smoke on each
31
+ line on Linux, plus Node 26 portability on macOS and Windows. `.nvmrc`
32
+ selects major 26 without requiring an exact patch. This is a maintained-line
33
+ policy, not a promise about every historical patch. The current
34
34
  conformance matrix is Playwright 1.62.1 and Vitest Browser 4.1.10 and 5.0.3.
35
35
  Package-boundary consumers run with a credential-free home directory and npm
36
36
  configuration, install only the peer family needed by each entry point, and
@@ -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.1",
3
+ "version": "1.2.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",