game-harness 1.0.0 → 1.1.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
@@ -117,6 +117,10 @@ hardcoded version string into a workflow file.
117
117
  clean-tarball consumer smoke tests. This is what CI runs; run it before
118
118
  every commit.
119
119
  - `pnpm test` — unit and contract tests only, for fast iteration.
120
+ - `pnpm test:chromium` — real headed Chromium checks of the launch profile
121
+ (needs `pnpm exec playwright install chromium`, and `xvfb-run -a` on a
122
+ Linux host without a display). CI runs it in its own `chromium` job; it is
123
+ not part of `pnpm verify`, so publishing never depends on a browser.
120
124
  - `pnpm --filter game-harness-docs validate` / `pnpm --filter
121
125
  game-harness-docs dev` — build or preview the Sourcey docs site in isolation.
122
126
  - `pnpm format` — apply Prettier.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ 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.1.0](https://github.com/jbcom/game-harness/compare/game-harness-v1.0.0...game-harness-v1.1.0) (2026-10-07)
8
+
9
+
10
+ ### Features
11
+
12
+ * accept Vitest 5 as a peer alongside Vitest 4 ([5d7a86c](https://github.com/jbcom/game-harness/commit/5d7a86c9e0b393e5e00818f89b03160cdd2c7370))
13
+ * **chromium:** keep background and occluded windows on schedule ([ba72078](https://github.com/jbcom/game-harness/commit/ba72078f8d3af5151d63e331eb84253213ac3a08))
14
+ * converge test-harness into game-harness and keep background windows on schedule ([b5295f9](https://github.com/jbcom/game-harness/commit/b5295f9c6fa421198e171b95358f19459a6f5f99))
15
+ * export VisualBatteryCommand from the package root ([ca6dd88](https://github.com/jbcom/game-harness/commit/ca6dd884935bd91ae811168a88e5a8bbbda0629c))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * harden release package install ([c74160d](https://github.com/jbcom/game-harness/commit/c74160dc78d43811c94c8249fae40ab9e6bb8632))
21
+ * publish releases from cd workflow ([f6f3b3e](https://github.com/jbcom/game-harness/commit/f6f3b3e5ac1ab8cf3670fb47cb98523f1ce4d1be))
22
+ * publish releases from cd workflow ([c7560ac](https://github.com/jbcom/game-harness/commit/c7560ac921b20ef706f761c14955088bfd07a7f7))
23
+
7
24
  ## [1.0.0](https://github.com/jbcom/game-harness/compare/game-harness-v0.5.0...game-harness-v1.0.0) (2026-08-24)
8
25
 
9
26
 
@@ -62,8 +79,7 @@ Commits by release-please.
62
79
 
63
80
  ### Added
64
81
 
65
- - Initial standalone public package extracted from the production browser-game
66
- verification harness.
82
+ - Initial standalone public package for browser-game verification.
67
83
  - Peer-isolated Playwright and Vitest Browser Mode configuration entry points.
68
84
  - Fail-closed silent-QA, production-runtime, visual-regression, Lighthouse, and
69
85
  release-ladder primitives.
package/MIGRATION.md ADDED
@@ -0,0 +1,137 @@
1
+ # Migrating from a scoped `test-harness` package
2
+
3
+ Game Harness grew out of a scoped package published privately as
4
+ `@your-scope/test-harness`. Every capability of that package now lives here, under
5
+ the same export names and subpaths. This guide maps each import, option and
6
+ binary, and lists the few places where Game Harness is stricter. The reasoning
7
+ behind each difference is in [docs/decisions.md](docs/decisions.md).
8
+
9
+ ## 1. Swap the dependency
10
+
11
+ ```sh
12
+ pnpm remove @your-scope/test-harness
13
+ pnpm add -D game-harness
14
+ ```
15
+
16
+ Game Harness is on the public npm registry, so a registry mapping for the old
17
+ scope is no longer needed once nothing else uses it. Keep the peers you
18
+ already install: `@playwright/test` for `/playwright` and
19
+ `/production-runtime`, and `vitest` with `@vitest/browser-playwright` for
20
+ `/vitest`. Vitest 4 (from 4.1.10) and Vitest 5 are both supported. Node 22 or
21
+ newer is required.
22
+
23
+ ## 2. Rewrite imports
24
+
25
+ Only the package name changes. Every subpath and named export is the same.
26
+
27
+ | Before | After |
28
+ | --------------------------------------------- | --------------------------------- |
29
+ | `@your-scope/test-harness` | `game-harness` |
30
+ | `@your-scope/test-harness/vitest` | `game-harness/vitest` |
31
+ | `@your-scope/test-harness/playwright` | `game-harness/playwright` |
32
+ | `@your-scope/test-harness/silent-qa` | `game-harness/silent-qa` |
33
+ | `@your-scope/test-harness/production-runtime` | `game-harness/production-runtime` |
34
+ | `@your-scope/test-harness/chromium` | `game-harness/chromium` |
35
+ | `@your-scope/test-harness/lighthouse` | `game-harness/lighthouse` |
36
+ | `@your-scope/test-harness/release-ladder` | `game-harness/release-ladder` |
37
+ | `@your-scope/test-harness/visual-battery` | `game-harness/visual-battery` |
38
+ | `@your-scope/test-harness/package.json` | `game-harness/package.json` |
39
+
40
+ A single search and replace of the specifier prefix is enough:
41
+
42
+ ```sh
43
+ git grep -lz '@your-scope/test-harness' | xargs -0 perl -pi -e 's#\@your-scope/test-harness#game-harness#g'
44
+ ```
45
+
46
+ Replace `your-scope` in these commands with the scope your project installed
47
+ the package under.
48
+
49
+ ### Export by export
50
+
51
+ | Subpath | Exports (identical names) |
52
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | root | `lighthouseAssertions`, `LighthouseAssertionsOverrides`, `LighthouseCiConfig`, `verifyReleaseLadder`, `ReleaseLadderStep`, `ReleaseLadderResult`, `VerifyReleaseLadderOptions`, `runVisualBattery`, `VisualBatteryError`, `VisualBatteryOptions` |
54
+ | `/vitest` | `defineBrowserTestConfig`, `BrowserTestConfigOptions`, `BrowserInstance` |
55
+ | `/playwright` | `definePlaywrightConfig`, `PlaywrightConfigOptions`, `DeviceTier`, `resolvePlaywrightPort`, `ResolvePlaywrightPortOptions`, `silentTestUrl`, `SilentTestUrlOptions`, `SilentQueryValue`, `openSilentGame`, `OpenSilentGameOptions` |
56
+ | `/silent-qa` | `activateSilentQa`, `isSilentQaRequested`, `isSilentQaActive`, `_resetSilentQaForTests`, `ActivateSilentQaOptions`, `SilentQaMarkerTarget`, `SILENT_QA_QUERY_PARAMETER`, `SILENT_QA_QUERY_VALUE`, `SILENT_QA_MARKER_ATTRIBUTE`, `SILENT_QA_MARKER_VALUE` |
57
+ | `/production-runtime` | `verifyProductionRuntime`, `ProductionRuntimeOptions`, `ProductionRuntimeServerOptions`, `ProductionRuntimeResult`, `ProductionRuntimeIssue`, `ProductionRuntimeVerificationError`, `findAvailableProductionPort`, `AvailableProductionPortOptions`, `readWebGLRenderer`, `requireHardwareWebGL`, `WebGLRendererInfo` |
58
+ | `/chromium` | `createChromiumLaunchProfile`, `ChromiumLaunchProfileOptions`, `ChromiumLaunchProfile`, `ChromiumGpuMode`, `ChromiumEnvironment` |
59
+ | `/lighthouse` | `lighthouseAssertions`, `LighthouseAssertionsOverrides`, `LighthouseCiConfig`, `LighthousePreset` |
60
+ | `/release-ladder` | `verifyReleaseLadder`, `ReleaseLadderStep`, `ReleaseLadderResult`, `VerifyReleaseLadderOptions` |
61
+ | `/visual-battery` | `runVisualBattery`, `VisualBatteryError`, `VisualBatteryOptions` |
62
+
63
+ New in Game Harness: `activateSilentQaAsync` (`/silent-qa`),
64
+ `VisualBatteryCommand` (`/visual-battery` and root) and
65
+ `CHROMIUM_ANTI_THROTTLING_ARGS` (`/chromium`).
66
+
67
+ ## 3. Options
68
+
69
+ Every option keeps its name and meaning:
70
+
71
+ | Options type | Options |
72
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `BrowserTestConfigOptions` | `contextOptions`, `gpuArgs`, `gpuMode`, `headless`, `ui`, `instances`, `optimizeDeps`, `setupFiles`, `name`, `include`, `fileParallelism` |
74
+ | `PlaywrightConfigOptions` | `testDir`, `basePath`, `port`, `webServerCommand`, `gpuMode`, `headless`, `deviceTiers`, `extraProjects`, `journeySpecs`, `ciTimeoutMultiplier`, `overrides` |
75
+ | `SilentTestUrlOptions` | `muteQueryParameter`, `muteQueryValue` |
76
+ | `OpenSilentGameOptions` | `markerSelector`, `markerAttribute`, `markerValue`, `markerTimeout`, `navigationOptions` |
77
+ | `ResolvePlaywrightPortOptions` | `localPort`, `environment` |
78
+ | `ProductionRuntimeOptions` | `url`, `server`, `browserLaunchOptions`, `gpuMode`, `pageOptions`, `silentParameters`, `silentOptions`, `localStorageSentinels`, `assertReady`, `assertSilentState`, `settleTimeMs` |
79
+ | `ProductionRuntimeServerOptions` | `command`, `args`, `cwd`, `env`, `readyUrl`, `startupTimeoutMs`, `shutdownTimeoutMs` |
80
+ | `AvailableProductionPortOptions` | `host` |
81
+ | `ActivateSilentQaOptions` | `search`, `queryParameter`, `markerTarget`, `markerAttribute`, `markerValue` |
82
+ | `VisualBatteryOptions` | `ci`, `cwd`, `testCommand`, `baselinesDir`, `baselineProfile`, `isolatedHarnessFiles`, `log`, `error` |
83
+ | `LighthouseAssertionsOverrides` | `staticDistDir`, `url`, `numberOfRuns`, `assertions` |
84
+ | `ChromiumLaunchProfileOptions` | `gpuMode`, `args`, `env` |
85
+ | `VerifyReleaseLadderOptions` | `log`, `error` |
86
+
87
+ Environment variables are unchanged: `CI`, `MULTIVIEW`, `VISUAL`, `JOURNEY`,
88
+ `PLAYWRIGHT_PORT`, `PW_PORT`, `PW_HEADLESS`, `PW_REUSE_SERVER`,
89
+ `PW_CHROMIUM_CHANNEL` and `VITE_VISUAL_BASELINE_PROFILE`.
90
+
91
+ ## 4. Binary
92
+
93
+ | Before | After |
94
+ | ----------------------------- | ----------------------------- |
95
+ | `test-harness-visual-battery` | `game-harness-visual-battery` |
96
+
97
+ The old binary name is still installed as an alias, so scripts keep working,
98
+ but new scripts should use `game-harness-visual-battery`. Arguments are the
99
+ same (`[harness-directory] [--ci]`). Unknown flags or a second directory now
100
+ exit with code 2 instead of being ignored.
101
+
102
+ ## 5. Where Game Harness is stricter
103
+
104
+ These are the only changes that can make a previously passing setup fail.
105
+ Each one fails loudly at configuration time with a message naming the
106
+ problem.
107
+
108
+ | Situation | What to do |
109
+ | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
110
+ | `testCommand` relies on a shell: `'FOO=1 pnpm test:browser'`, `&&`, pipes or quoted arguments | Move the shell logic into a package script and pass its name (`'pnpm test:visual'`), or pass `{ command: 'pnpm', args: ['test:browser'] }`. No shell is ever started. |
111
+ | `PLAYWRIGHT_PORT` or `PW_PORT` set to a non-port value | Fix or unset it. Game Harness throws instead of falling back to the derived port. |
112
+ | `activateSilentQa` given a callback that returns a promise | Use `await activateSilentQaAsync(mute)`. The readiness marker then appears only after the mute has resolved. |
113
+ | `basePath` containing `?`, `#`, `.`/`..` segments or encoded slashes | Pass a plain pathname. Leading, trailing and repeated slashes are normalized for you. |
114
+ | Empty `deviceTiers`, an unknown tier, or `ciTimeoutMultiplier` that is not positive | Pass at least one of `desktop`, `mobile`, `tablet`, `foldable`, `ultrawide`, and a positive multiplier. |
115
+ | Empty Vitest `name`, `instances` or `include` | Omit the option to get the default, or pass a non-empty value. |
116
+ | Lighthouse override with an empty `staticDistDir`, an empty `url` list, or `numberOfRuns < 1` | Omit the override or pass a valid value. |
117
+ | Code that builds a `LighthouseCiConfig` by hand without `ci.collect.staticDistDir` | Add it. The result of `lighthouseAssertions()` always includes it. |
118
+ | `baselinesDir` absolute, or harness or baseline directory outside `cwd` | Use a path relative to, and inside, `cwd`. |
119
+ | A visual-battery run that writes no PNG baselines | It now fails. Make sure the harness actually takes screenshots. |
120
+
121
+ ## 6. Behaviour you get for free
122
+
123
+ - Every launch profile adds `--disable-background-timer-throttling`,
124
+ `--disable-renderer-backgrounding` and
125
+ `--disable-backgrounding-occluded-windows`, so background and occluded
126
+ windows keep their timers on schedule when several headed browsers run at
127
+ once. If a custom launch passes its own `args`, they are merged with these,
128
+ and `--mute-audio` is still applied last.
129
+ - Harness files run in a sorted, deterministic order.
130
+ - Visual-battery commands run without a shell, with Windows `.cmd` shims
131
+ resolved for you.
132
+
133
+ ## 7. Verify
134
+
135
+ Run the consumer's own full gate (type check, unit tests, browser tests and
136
+ end-to-end tests). A green unit run alone does not prove the browser
137
+ configuration still loads.
package/README.md CHANGED
@@ -127,7 +127,7 @@ supported. CI pins the primary gate to the version in `.nvmrc` (currently
127
127
  24.19.0, the latest Node 24 LTS patch) and additionally runs the full test
128
128
  and build suite against Node 22 on Linux to prove the floor of that range,
129
129
  alongside macOS and Windows portability on the pinned version. The current
130
- conformance matrix is Playwright 1.62.1 and Vitest Browser 4.1.10.
130
+ conformance matrix is Playwright 1.62.1 and Vitest Browser 4.1.10 and 5.0.3.
131
131
  Package-boundary consumers run with a credential-free home directory and npm
132
132
  configuration, install only the peer family needed by each entry point, and
133
133
  exercise ESM, CommonJS, the CLI, silent runtime markers, and Chromium launch
@@ -257,7 +257,14 @@ SwiftShader explicitly; `linux-hardware-vulkan` applies the reviewed
257
257
  Mesa/ANGLE flags plus `EGL_PLATFORM=surfaceless` for a runner exposing
258
258
  `/dev/dri/renderD128`) and always de-duplicates and appends `--mute-audio`
259
259
  last, so a caller-supplied arg list can never accidentally drop the silence
260
- guard.
260
+ guard. Every profile also carries `CHROMIUM_ANTI_THROTTLING_ARGS`
261
+ (`--disable-background-timer-throttling`, `--disable-renderer-backgrounding`,
262
+ `--disable-backgrounding-occluded-windows`), so background and occluded
263
+ windows keep their timers on schedule when several headed browsers run at
264
+ once; Page Visibility still reports `hidden`.
265
+
266
+ Moving from the scoped `test-harness` package this one grew out of? See
267
+ [MIGRATION.md](MIGRATION.md).
261
268
 
262
269
  Every browser launched by `definePlaywrightConfig()` or
263
270
  `defineBrowserTestConfig()` receives Chromium's `--mute-audio` argument as a
@@ -309,8 +316,8 @@ Before publishing, run `pnpm verify` under the pinned release toolchain. The
309
316
  package verifier uses npm 11.17.0 directly from the package directory, packs a
310
317
  tarball, and installs it into credential-free temporary consumers for the
311
318
  peer-free root, Playwright/production-runtime, and Vitest Browser boundaries.
312
- The release workflow repeats the full gate before publishing with npm
313
- provenance.
319
+ The tagged-release job in `cd.yml` repeats the full gate before publishing
320
+ with npm provenance through npm trusted publishing (OIDC).
314
321
 
315
322
  ## Production runtime verification
316
323
 
@@ -556,8 +563,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution and commit guidance.
556
563
  ## Releases and support
557
564
 
558
565
  Conventional commits on `main` are collected into a release pull request by
559
- release-please. Merging that pull request creates the GitHub release and
560
- publishes the exact tag to npm with provenance after `pnpm verify` passes.
566
+ release-please. Merging that pull request creates the GitHub release; its
567
+ published-release event starts `cd.yml`, which verifies and publishes the exact
568
+ tag to npm through OIDC with provenance.
561
569
  Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
562
570
 
563
571
  Report defects and feature requests through the repository issue forms. Report
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHROMIUM_ANTI_THROTTLING_ARGS = void 0;
3
4
  exports.createChromiumLaunchProfile = createChromiumLaunchProfile;
4
5
  const GPU_ARGS = {
5
6
  auto: [],
@@ -12,13 +13,27 @@ const GPU_ARGS = {
12
13
  ],
13
14
  };
14
15
  /**
15
- * Builds the shared Chromium renderer and silence profile without deciding
16
- * whether the browser is headed. Callers own that explicit choice.
16
+ * Keeps timers and rendering on schedule in pages Chromium considers
17
+ * backgrounded. Several headed windows commonly run at once during a test
18
+ * suite; without these switches a background tab's timers are coalesced to
19
+ * one wake-up per second, and an occluded window or backgrounded renderer is
20
+ * deprioritised, so suites time out for reasons unrelated to the game.
21
+ * Page Visibility still reports `hidden`, so visibility handling stays
22
+ * testable.
23
+ */
24
+ exports.CHROMIUM_ANTI_THROTTLING_ARGS = Object.freeze([
25
+ '--disable-background-timer-throttling',
26
+ '--disable-renderer-backgrounding',
27
+ '--disable-backgrounding-occluded-windows',
28
+ ]);
29
+ /**
30
+ * Builds the shared Chromium renderer, scheduling and silence profile without
31
+ * deciding whether the browser is headed. Callers own that explicit choice.
17
32
  */
18
33
  function createChromiumLaunchProfile(options = {}) {
19
34
  const gpuMode = options.gpuMode ?? 'auto';
20
35
  const args = [
21
- ...new Set([...GPU_ARGS[gpuMode], ...(options.args ?? [])].filter((argument) => argument !== '--mute-audio')),
36
+ ...new Set([...GPU_ARGS[gpuMode], ...exports.CHROMIUM_ANTI_THROTTLING_ARGS, ...(options.args ?? [])].filter((argument) => argument !== '--mute-audio')),
22
37
  '--mute-audio',
23
38
  ];
24
39
  if (gpuMode === 'linux-hardware-vulkan') {
@@ -7,7 +7,7 @@ exports.verifyReleaseLadder = verifyReleaseLadder;
7
7
  * screenshots → native sync, or whatever a given repo's ladder is), run in
8
8
  * sequence and stopped at the first failure with a labeled summary.
9
9
  *
10
- * Generalizes the reach-for-the-sky pattern of ~17 discrete
10
+ * Generalizes the common pattern of many discrete
11
11
  * `node scripts/verify-X.mjs` files composed via a shell `&&` chain into a
12
12
  * single reusable primitive: each step is a plain function (sync or async),
13
13
  * so a repo can inline its logic or delegate to existing scripts via
@@ -9,13 +9,27 @@ const GPU_ARGS = {
9
9
  ],
10
10
  };
11
11
  /**
12
- * Builds the shared Chromium renderer and silence profile without deciding
13
- * whether the browser is headed. Callers own that explicit choice.
12
+ * Keeps timers and rendering on schedule in pages Chromium considers
13
+ * backgrounded. Several headed windows commonly run at once during a test
14
+ * suite; without these switches a background tab's timers are coalesced to
15
+ * one wake-up per second, and an occluded window or backgrounded renderer is
16
+ * deprioritised, so suites time out for reasons unrelated to the game.
17
+ * Page Visibility still reports `hidden`, so visibility handling stays
18
+ * testable.
19
+ */
20
+ export const CHROMIUM_ANTI_THROTTLING_ARGS = Object.freeze([
21
+ '--disable-background-timer-throttling',
22
+ '--disable-renderer-backgrounding',
23
+ '--disable-backgrounding-occluded-windows',
24
+ ]);
25
+ /**
26
+ * Builds the shared Chromium renderer, scheduling and silence profile without
27
+ * deciding whether the browser is headed. Callers own that explicit choice.
14
28
  */
15
29
  export function createChromiumLaunchProfile(options = {}) {
16
30
  const gpuMode = options.gpuMode ?? 'auto';
17
31
  const args = [
18
- ...new Set([...GPU_ARGS[gpuMode], ...(options.args ?? [])].filter((argument) => argument !== '--mute-audio')),
32
+ ...new Set([...GPU_ARGS[gpuMode], ...CHROMIUM_ANTI_THROTTLING_ARGS, ...(options.args ?? [])].filter((argument) => argument !== '--mute-audio')),
19
33
  '--mute-audio',
20
34
  ];
21
35
  if (gpuMode === 'linux-hardware-vulkan') {
@@ -4,7 +4,7 @@
4
4
  * screenshots → native sync, or whatever a given repo's ladder is), run in
5
5
  * sequence and stopped at the first failure with a labeled summary.
6
6
  *
7
- * Generalizes the reach-for-the-sky pattern of ~17 discrete
7
+ * Generalizes the common pattern of many discrete
8
8
  * `node scripts/verify-X.mjs` files composed via a shell `&&` chain into a
9
9
  * single reusable primitive: each step is a plain function (sync or async),
10
10
  * so a repo can inline its logic or delegate to existing scripts via
@@ -15,13 +15,27 @@ export interface ChromiumLaunchProfileOptions {
15
15
  env?: Readonly<ChromiumEnvironment>;
16
16
  }
17
17
  export interface ChromiumLaunchProfile {
18
- /** De-duplicated Chromium launch arguments for the selected `gpuMode`, always ending in `--mute-audio`. */
18
+ /**
19
+ * De-duplicated Chromium launch arguments: the `gpuMode` renderer flags, the
20
+ * {@link CHROMIUM_ANTI_THROTTLING_ARGS}, any caller `args`, and always
21
+ * `--mute-audio` last.
22
+ */
19
23
  args: string[];
20
24
  /** Present when `env` overrides were supplied or `gpuMode` is `linux-hardware-vulkan` (which also sets `EGL_PLATFORM`); merged on top of `process.env`. */
21
25
  env?: ChromiumEnvironment;
22
26
  }
23
27
  /**
24
- * Builds the shared Chromium renderer and silence profile without deciding
25
- * whether the browser is headed. Callers own that explicit choice.
28
+ * Keeps timers and rendering on schedule in pages Chromium considers
29
+ * backgrounded. Several headed windows commonly run at once during a test
30
+ * suite; without these switches a background tab's timers are coalesced to
31
+ * one wake-up per second, and an occluded window or backgrounded renderer is
32
+ * deprioritised, so suites time out for reasons unrelated to the game.
33
+ * Page Visibility still reports `hidden`, so visibility handling stays
34
+ * testable.
35
+ */
36
+ export declare const CHROMIUM_ANTI_THROTTLING_ARGS: readonly string[];
37
+ /**
38
+ * Builds the shared Chromium renderer, scheduling and silence profile without
39
+ * deciding whether the browser is headed. Callers own that explicit choice.
26
40
  */
27
41
  export declare function createChromiumLaunchProfile(options?: ChromiumLaunchProfileOptions): ChromiumLaunchProfile;
@@ -15,13 +15,27 @@ export interface ChromiumLaunchProfileOptions {
15
15
  env?: Readonly<ChromiumEnvironment>;
16
16
  }
17
17
  export interface ChromiumLaunchProfile {
18
- /** De-duplicated Chromium launch arguments for the selected `gpuMode`, always ending in `--mute-audio`. */
18
+ /**
19
+ * De-duplicated Chromium launch arguments: the `gpuMode` renderer flags, the
20
+ * {@link CHROMIUM_ANTI_THROTTLING_ARGS}, any caller `args`, and always
21
+ * `--mute-audio` last.
22
+ */
19
23
  args: string[];
20
24
  /** Present when `env` overrides were supplied or `gpuMode` is `linux-hardware-vulkan` (which also sets `EGL_PLATFORM`); merged on top of `process.env`. */
21
25
  env?: ChromiumEnvironment;
22
26
  }
23
27
  /**
24
- * Builds the shared Chromium renderer and silence profile without deciding
25
- * whether the browser is headed. Callers own that explicit choice.
28
+ * Keeps timers and rendering on schedule in pages Chromium considers
29
+ * backgrounded. Several headed windows commonly run at once during a test
30
+ * suite; without these switches a background tab's timers are coalesced to
31
+ * one wake-up per second, and an occluded window or backgrounded renderer is
32
+ * deprioritised, so suites time out for reasons unrelated to the game.
33
+ * Page Visibility still reports `hidden`, so visibility handling stays
34
+ * testable.
35
+ */
36
+ export declare const CHROMIUM_ANTI_THROTTLING_ARGS: readonly string[];
37
+ /**
38
+ * Builds the shared Chromium renderer, scheduling and silence profile without
39
+ * deciding whether the browser is headed. Callers own that explicit choice.
26
40
  */
27
41
  export declare function createChromiumLaunchProfile(options?: ChromiumLaunchProfileOptions): ChromiumLaunchProfile;
@@ -1,3 +1,3 @@
1
1
  export { type LighthouseAssertionsOverrides, type LighthouseCiConfig, lighthouseAssertions, } from './lighthouse.js';
2
2
  export { type ReleaseLadderResult, type ReleaseLadderStep, type VerifyReleaseLadderOptions, verifyReleaseLadder, } from './release-ladder.js';
3
- export { runVisualBattery, VisualBatteryError, type VisualBatteryOptions, } from './visual-battery.js';
3
+ export { runVisualBattery, type VisualBatteryCommand, VisualBatteryError, type VisualBatteryOptions, } from './visual-battery.js';
@@ -1,3 +1,3 @@
1
1
  export { type LighthouseAssertionsOverrides, type LighthouseCiConfig, lighthouseAssertions, } from './lighthouse.js';
2
2
  export { type ReleaseLadderResult, type ReleaseLadderStep, type VerifyReleaseLadderOptions, verifyReleaseLadder, } from './release-ladder.js';
3
- export { runVisualBattery, VisualBatteryError, type VisualBatteryOptions, } from './visual-battery.js';
3
+ export { runVisualBattery, type VisualBatteryCommand, VisualBatteryError, type VisualBatteryOptions, } from './visual-battery.js';
@@ -26,7 +26,7 @@ export interface VerifyReleaseLadderOptions {
26
26
  * screenshots → native sync, or whatever a given repo's ladder is), run in
27
27
  * sequence and stopped at the first failure with a labeled summary.
28
28
  *
29
- * Generalizes the reach-for-the-sky pattern of ~17 discrete
29
+ * Generalizes the common pattern of many discrete
30
30
  * `node scripts/verify-X.mjs` files composed via a shell `&&` chain into a
31
31
  * single reusable primitive: each step is a plain function (sync or async),
32
32
  * so a repo can inline its logic or delegate to existing scripts via
@@ -26,7 +26,7 @@ export interface VerifyReleaseLadderOptions {
26
26
  * screenshots → native sync, or whatever a given repo's ladder is), run in
27
27
  * sequence and stopped at the first failure with a labeled summary.
28
28
  *
29
- * Generalizes the reach-for-the-sky pattern of ~17 discrete
29
+ * Generalizes the common pattern of many discrete
30
30
  * `node scripts/verify-X.mjs` files composed via a shell `&&` chain into a
31
31
  * single reusable primitive: each step is a plain function (sync or async),
32
32
  * so a repo can inline its logic or delegate to existing scripts via
@@ -29,8 +29,8 @@ either framework.
29
29
  2. When a server is configured, its readiness URL must be unreachable first.
30
30
  The command is spawned directly with an argument array and must bind with
31
31
  strict-port behavior.
32
- 3. A fresh browser launches with the selected renderer profile and exactly one
33
- final `--mute-audio` argument.
32
+ 3. A fresh browser launches with the selected renderer profile, the
33
+ anti-throttling switches, and exactly one final `--mute-audio` argument.
34
34
  4. Local-storage sentinels are installed before application code.
35
35
  5. `openSilentGame()` adds the runtime-only mute query and waits for the
36
36
  application readiness marker.
@@ -0,0 +1,154 @@
1
+ ---
2
+ title: Decisions
3
+ description: Why the package is shaped the way it is, and how a predecessor harness converged into it.
4
+ ---
5
+
6
+ Each entry records a decision, the reason for it, and what it means for a
7
+ consumer. Newest first.
8
+
9
+ ## Converging a predecessor `test-harness` package
10
+
11
+ This entry compares the supported migration paths export by export so that
12
+ consumers of an earlier `test-harness` package can move to Game Harness. The step-by-step
13
+ consumer mapping is in [MIGRATION.md](https://github.com/jbcom/game-harness/blob/main/MIGRATION.md).
14
+
15
+ **Decision:** Game Harness is the one canonical harness. Where both packages
16
+ implement the same thing, Game Harness keeps its own public shape; anything the
17
+ predecessor did that Game Harness did not is added here, with tests.
18
+
19
+ ### Method
20
+
21
+ 1. Diffed every source file of the predecessor's last release against this
22
+ repository's `main`.
23
+ 2. Ran the predecessor's own unit suite against Game Harness's source. Every
24
+ behavioural test passed (97 of 107). The ten failures were all in the
25
+ visual battery and mocked `execSync`, an implementation detail Game Harness
26
+ replaced with shell-free `execFileSync`; the behaviour those tests describe
27
+ is covered by Game Harness's own suite. The predecessor's package-contract
28
+ test checks its own package name and is not applicable.
29
+
30
+ ### Entry points
31
+
32
+ | Subpath | Predecessor | Game Harness | Notes |
33
+ | --------------------- | ----------- | ------------ | ----------------------------------------------------------- |
34
+ | `.` (root) | yes | yes | Same re-exports: Lighthouse, release ladder, visual battery |
35
+ | `/vitest` | yes | yes | Same function; Game Harness validates its inputs |
36
+ | `/playwright` | yes | yes | Same functions; Game Harness validates and normalizes |
37
+ | `/silent-qa` | yes | yes | Game Harness adds `activateSilentQaAsync` |
38
+ | `/production-runtime` | yes | yes | Same functions; Game Harness validates URLs and durations |
39
+ | `/chromium` | yes | yes | Game Harness adds `CHROMIUM_ANTI_THROTTLING_ARGS` |
40
+ | `/lighthouse` | yes | yes | Same function; result type always carries `staticDistDir` |
41
+ | `/release-ladder` | yes | yes | Identical behaviour |
42
+ | `/visual-battery` | yes | yes | Game Harness adds `VisualBatteryCommand` and path checks |
43
+ | `/package.json` | yes | yes | |
44
+
45
+ Both packages ship ESM and CommonJS builds. Game Harness additionally ships
46
+ separate `.d.cts` declarations for `require` consumers.
47
+
48
+ ### Exports
49
+
50
+ Every named export of the predecessor exists in Game Harness under the same
51
+ name and subpath. Game Harness adds three:
52
+
53
+ | Export | Subpath | Why |
54
+ | ------------------------------- | ----------------------- | -------------------------------------------------------------------- |
55
+ | `activateSilentQaAsync` | `/silent-qa` | Publishes the readiness marker only after an async mute resolves |
56
+ | `VisualBatteryCommand` | `/visual-battery`, root | Runs the browser suite without a shell |
57
+ | `CHROMIUM_ANTI_THROTTLING_ARGS` | `/chromium` | The scheduling switches every launch profile now carries (see below) |
58
+
59
+ ### Bins
60
+
61
+ | Predecessor | Game Harness |
62
+ | ----------------------------- | ----------------------------------------------------------- |
63
+ | `test-harness-visual-battery` | `game-harness-visual-battery`, and the old name as an alias |
64
+
65
+ Game Harness's CLI also rejects unknown flags and more than one positional
66
+ directory (exit code 2) instead of silently ignoring them.
67
+
68
+ ### Peers
69
+
70
+ | Peer | Predecessor | Game Harness |
71
+ | ---------------------------- | ------------- | ------------- |
72
+ | `@playwright/test` | `>=1.62.1 <2` | `>=1.62.1 <2` |
73
+ | `vitest` | `>=4.1.10 <5` | `>=4.1.10 <6` |
74
+ | `@vitest/browser-playwright` | `>=4.1.10 <5` | `>=4.1.10 <6` |
75
+ | Node (`engines`) | `>=24.19.0` | `>=22` |
76
+
77
+ All framework peers stay optional; the root never loads them. CI proves the
78
+ packed tarball against Vitest 4.1.10 and 5.0.3.
79
+
80
+ ### Where the shapes differ
81
+
82
+ Game Harness's shape wins in each case. The consumer-facing consequence is
83
+ listed in MIGRATION.md.
84
+
85
+ - **Visual battery `testCommand`.** The predecessor passed a string to a
86
+ shell, so shell syntax (`FOO=1 pnpm …`, `&&`, quoting) worked by accident.
87
+ Game Harness never starts a shell: a string is split on whitespace, or a
88
+ `{ command, args }` object is executed directly. Kept because a shell turns
89
+ a configuration value into a command-injection surface and behaves
90
+ differently on Windows.
91
+ - **Strict input validation.** Game Harness throws a `TypeError` for an
92
+ invalid `PLAYWRIGHT_PORT`/`PW_PORT` (the predecessor silently fell back to
93
+ the derived port), an empty or unknown `deviceTiers` list, a non-positive
94
+ `ciTimeoutMultiplier`, a `basePath` with a query, fragment or traversal, an
95
+ empty Vitest project name, `instances` or `include`, and empty or invalid
96
+ Lighthouse overrides. Kept because a misconfigured gate that silently
97
+ "passes" is not release evidence.
98
+ - **Async mute callbacks.** The predecessor's `activateSilentQa` accepted a
99
+ promise-returning callback and published the readiness marker before the
100
+ mute finished. Game Harness throws and points to `activateSilentQaAsync`.
101
+ - **`LighthouseCiConfig.ci.collect.staticDistDir`** is required in the result
102
+ type. Every preset already sets it, so only code that builds the type by
103
+ hand without it is affected.
104
+ - **Visual battery paths.** Game Harness requires the harness and baseline
105
+ directories to stay inside `cwd`, sorts harness files for a deterministic
106
+ run order, and fails when a run produces no PNG baselines.
107
+
108
+ ### Deliberately not carried over
109
+
110
+ - **The predecessor's repository tooling:** its Gitea release workflow,
111
+ release-label script, anonymous-registry environment helper and the test
112
+ that pinned that workflow. Game Harness publishes from GitHub with npm
113
+ trusted publishing and provenance, so none of it applies.
114
+ - **Shell execution for `testCommand`**, for the reason above. A consumer
115
+ that needs environment variables or chained commands puts them in a
116
+ package script and passes that script's name.
117
+
118
+ ## Every launch profile disables background throttling
119
+
120
+ **Decision:** `createChromiumLaunchProfile()` always adds
121
+ `--disable-background-timer-throttling`, `--disable-renderer-backgrounding`
122
+ and `--disable-backgrounding-occluded-windows`, exported as
123
+ `CHROMIUM_ANTI_THROTTLING_ARGS`. Every Playwright project, Vitest Browser
124
+ instance and production-runtime launch inherits them.
125
+
126
+ **Why:** a browser suite commonly runs several headed windows at once. A tab
127
+ Chromium considers backgrounded has its timers coalesced to one wake-up per
128
+ second, and an occluded window or backgrounded renderer is deprioritised, so
129
+ suites time out for reasons that have nothing to do with the game.
130
+
131
+ Playwright adds the same switches to its own default arguments today, but the
132
+ profile is the package's contract with every launcher it serves: a custom
133
+ script, a launcher that does not use Playwright, or a caller that passes
134
+ `ignoreDefaultArgs`. Making the switches explicit keeps the guarantee from
135
+ depending on another tool's defaults. They do not change Page Visibility: a
136
+ backgrounded page still reports `hidden`, so a game's pause-on-hide logic
137
+ remains testable.
138
+
139
+ **Evidence:** `pnpm test:chromium` launches headed Chromium over the raw
140
+ DevTools protocol (Playwright's own switches and focus emulation would mask
141
+ the effect), opens a 50 ms timer in one tab and a second tab in front of it,
142
+ and samples the background tab for three seconds. With the profile the timer
143
+ keeps its schedule; a control case without the switches shows the one-second
144
+ throttling, which proves the scenario really backgrounds the tab.
145
+
146
+ ## Vitest 4 and 5
147
+
148
+ **Decision:** the `vitest` and `@vitest/browser-playwright` peers accept
149
+ `>=4.1.10 <6`. Development runs on Vitest 5, and the packed-consumer smoke
150
+ runs once against 4.1.10 (the floor) and once against 5.0.3.
151
+
152
+ **Why:** consumers are moving to Vitest 5, and the config fragment
153
+ `defineBrowserTestConfig()` emits is valid for both majors without a code
154
+ change.
@@ -31,7 +31,7 @@ supported. CI pins the primary gate to the version in `.nvmrc` (currently
31
31
  24.19.0, the latest Node 24 LTS patch) and additionally runs the full test
32
32
  and build suite against Node 22 on Linux to prove the floor of that range,
33
33
  alongside macOS and Windows portability on the pinned version. The current
34
- conformance matrix is Playwright 1.62.1 and Vitest Browser 4.1.10.
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
37
37
  exercise ESM, CommonJS, the CLI, silent runtime markers, and Chromium launch
@@ -24,6 +24,15 @@ exposing `/dev/dri/renderD128`) and always de-duplicates and appends
24
24
  `--mute-audio` last, so a caller-supplied arg list can never accidentally
25
25
  drop the silence guard.
26
26
 
27
+ Every profile also carries `CHROMIUM_ANTI_THROTTLING_ARGS`:
28
+ `--disable-background-timer-throttling`, `--disable-renderer-backgrounding`
29
+ and `--disable-backgrounding-occluded-windows`. When several headed windows
30
+ run at once, Chromium would otherwise coalesce a background tab's timers to
31
+ one wake-up per second and deprioritise occluded windows, and suites time out
32
+ for reasons unrelated to the game. Page Visibility is unaffected: a
33
+ backgrounded page still reports `hidden`, so pause-on-hide logic stays
34
+ testable.
35
+
27
36
  Every browser launched by `definePlaywrightConfig()` or
28
37
  `defineBrowserTestConfig()` receives Chromium's `--mute-audio` argument as a
29
38
  defense-in-depth guard, including projects with custom launch options. The
@@ -23,8 +23,9 @@ description: Common failure modes and their causes.
23
23
  ## Releases and support
24
24
 
25
25
  Conventional commits on `main` are collected into a release pull request by
26
- release-please. Merging that pull request creates the GitHub release and
27
- publishes the exact tag to npm with provenance after `pnpm verify` passes.
26
+ release-please. Merging that pull request creates the GitHub release; its
27
+ published-release event starts `cd.yml`, which verifies and publishes the exact
28
+ tag to npm through OIDC with provenance.
28
29
  Changes are recorded in the [CHANGELOG](https://github.com/jbcom/game-harness/blob/main/CHANGELOG.md).
29
30
 
30
31
  Report defects and feature requests through the
@@ -65,7 +65,7 @@ export default defineConfig({
65
65
  },
66
66
  {
67
67
  group: 'Reference',
68
- pages: ['architecture', 'reference/troubleshooting'],
68
+ pages: ['architecture', 'decisions', 'reference/troubleshooting'],
69
69
  },
70
70
  {
71
71
  group: 'Project',
package/llms.txt CHANGED
@@ -23,6 +23,7 @@ agent-oriented quick reference before writing code against this package.
23
23
  - [Visual battery guide](https://jonbogaty.com/game-harness/guides/visual-battery/): Deterministic screenshot baseline orchestration.
24
24
  - [Lighthouse presets & release ladder guide](https://jonbogaty.com/game-harness/guides/lighthouse-and-release-ladder/): CI Lighthouse assertions and the release-step orchestrator.
25
25
  - [Architecture reference](https://jonbogaty.com/game-harness/reference/architecture/): Module boundaries, runtime evidence flow, build/distribution guarantees.
26
+ - [Decisions reference](https://jonbogaty.com/game-harness/decisions/): Migration and compatibility decisions and their rationale.
26
27
  - [Troubleshooting reference](https://jonbogaty.com/game-harness/reference/troubleshooting/): Common failure modes and releases/support.
27
28
 
28
29
  ## Optional
@@ -3,7 +3,7 @@
3
3
  # render its first screen" flow; add game-specific flows alongside it as
4
4
  # separate .maestro/*.yaml files (they're app-specific user journeys, not
5
5
  # shareable config — see game-harness's package notes).
6
- appId: __APP_ID__ # e.g. com.jbcom.kuroga — must match capacitor.config.ts's appId
6
+ appId: __APP_ID__ # e.g. com.example.game — must match capacitor.config.ts's appId
7
7
  ---
8
8
  - launchApp
9
9
  - assertVisible:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "game-harness",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",
@@ -16,6 +16,7 @@
16
16
  "docs",
17
17
  "llms.txt",
18
18
  "maestro",
19
+ "MIGRATION.md",
19
20
  "LICENSE",
20
21
  "README.md"
21
22
  ],
@@ -125,8 +126,8 @@
125
126
  },
126
127
  "peerDependencies": {
127
128
  "@playwright/test": ">=1.62.1 <2",
128
- "@vitest/browser-playwright": ">=4.1.10 <5",
129
- "vitest": ">=4.1.10 <5"
129
+ "@vitest/browser-playwright": ">=4.1.10 <6",
130
+ "vitest": ">=4.1.10 <6"
130
131
  },
131
132
  "peerDependenciesMeta": {
132
133
  "@playwright/test": {
@@ -144,14 +145,14 @@
144
145
  "@playwright/test": "1.62.1",
145
146
  "@types/cross-spawn": "6.0.6",
146
147
  "@types/node": "24.13.3",
147
- "@vitest/browser-playwright": "4.1.10",
148
- "@vitest/coverage-v8": "4.1.10",
148
+ "@vitest/browser-playwright": "5.0.3",
149
+ "@vitest/coverage-v8": "5.0.3",
149
150
  "oxlint": "1.79.0",
150
151
  "prettier": "3.9.6",
151
152
  "publint": "0.3.24",
152
153
  "rimraf": "6.1.3",
153
154
  "typescript": "7.0.2",
154
- "vitest": "4.1.10"
155
+ "vitest": "5.0.3"
155
156
  },
156
157
  "engines": {
157
158
  "node": ">=22"
@@ -189,6 +190,7 @@
189
190
  "lint": "oxlint --deny-warnings src tests scripts",
190
191
  "package:check": "publint && attw --pack . --profile node16",
191
192
  "test": "vitest run",
193
+ "test:chromium": "vitest run --config vitest.chromium.config.ts",
192
194
  "test:package": "node scripts/verify-package-boundaries.mjs",
193
195
  "typecheck": "tsc -p tsconfig.json --noEmit",
194
196
  "verify": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run coverage && pnpm run build && pnpm run package:check && pnpm run test:package"