game-harness 1.0.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.
Files changed (69) hide show
  1. package/AGENTS.md +140 -0
  2. package/CHANGELOG.md +69 -0
  3. package/LICENSE +21 -0
  4. package/README.md +568 -0
  5. package/bin/test-harness-visual-battery.mjs +6 -0
  6. package/dist/cjs/bin/visual-battery.js +43 -0
  7. package/dist/cjs/browser-config.js +83 -0
  8. package/dist/cjs/chromium-launch.js +37 -0
  9. package/dist/cjs/index.js +10 -0
  10. package/dist/cjs/lighthouse.js +73 -0
  11. package/dist/cjs/package.json +3 -0
  12. package/dist/cjs/playwright-config.js +303 -0
  13. package/dist/cjs/production-runtime.js +315 -0
  14. package/dist/cjs/release-ladder.js +40 -0
  15. package/dist/cjs/silent-qa.js +75 -0
  16. package/dist/cjs/visual-battery.js +247 -0
  17. package/dist/esm/bin/visual-battery.js +41 -0
  18. package/dist/esm/browser-config.js +80 -0
  19. package/dist/esm/chromium-launch.js +34 -0
  20. package/dist/esm/index.js +3 -0
  21. package/dist/esm/lighthouse.js +70 -0
  22. package/dist/esm/playwright-config.js +292 -0
  23. package/dist/esm/production-runtime.js +304 -0
  24. package/dist/esm/release-ladder.js +37 -0
  25. package/dist/esm/silent-qa.js +67 -0
  26. package/dist/esm/visual-battery.js +239 -0
  27. package/dist/types/bin/visual-battery.d.cts +2 -0
  28. package/dist/types/bin/visual-battery.d.ts +2 -0
  29. package/dist/types/browser-config.d.cts +100 -0
  30. package/dist/types/browser-config.d.ts +100 -0
  31. package/dist/types/chromium-launch.d.cts +27 -0
  32. package/dist/types/chromium-launch.d.ts +27 -0
  33. package/dist/types/index.d.cts +3 -0
  34. package/dist/types/index.d.ts +3 -0
  35. package/dist/types/lighthouse.d.cts +44 -0
  36. package/dist/types/lighthouse.d.ts +44 -0
  37. package/dist/types/playwright-config.d.cts +122 -0
  38. package/dist/types/playwright-config.d.ts +122 -0
  39. package/dist/types/production-runtime.d.cts +105 -0
  40. package/dist/types/production-runtime.d.ts +105 -0
  41. package/dist/types/release-ladder.d.cts +38 -0
  42. package/dist/types/release-ladder.d.ts +38 -0
  43. package/dist/types/silent-qa.d.cts +47 -0
  44. package/dist/types/silent-qa.d.ts +47 -0
  45. package/dist/types/visual-battery.d.cts +69 -0
  46. package/dist/types/visual-battery.d.ts +69 -0
  47. package/docs/404.md +17 -0
  48. package/docs/agent-guide.md +10 -0
  49. package/docs/architecture.md +81 -0
  50. package/docs/assets/game-harness-hero.webp +0 -0
  51. package/docs/changelog.md +9 -0
  52. package/docs/contributing.md +21 -0
  53. package/docs/entry-points.md +41 -0
  54. package/docs/getting-started.md +42 -0
  55. package/docs/guides/chromium-and-silent-qa.md +72 -0
  56. package/docs/guides/lighthouse-and-release-ladder.md +62 -0
  57. package/docs/guides/playwright.md +58 -0
  58. package/docs/guides/production-runtime.md +61 -0
  59. package/docs/guides/visual-battery.md +58 -0
  60. package/docs/guides/vitest.md +41 -0
  61. package/docs/introduction.md +29 -0
  62. package/docs/package.json +13 -0
  63. package/docs/quick-start.md +47 -0
  64. package/docs/reference/troubleshooting.md +33 -0
  65. package/docs/security.md +15 -0
  66. package/docs/sourcey.config.ts +79 -0
  67. package/llms.txt +32 -0
  68. package/maestro/smoke.template.yaml +11 -0
  69. package/package.json +196 -0
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Contributing
3
+ description: Development, validation, and pull-request expectations.
4
+ ---
5
+
6
+ Game Harness uses pnpm and Node from `.nvmrc` (Node 22 or newer).
7
+
8
+ ```sh
9
+ mise install # or: nvm use && corepack enable
10
+ pnpm install --frozen-lockfile
11
+ pnpm verify
12
+ ```
13
+
14
+ Write a focused test, implement the change, and run `pnpm verify` before
15
+ opening a pull request. Use a Conventional Commit (`fix:`, `feat:`, `docs:`,
16
+ `refactor:`, `test:`, or `chore:`); release-please owns release versions and
17
+ the changelog.
18
+
19
+ Open a same-upstream branch and PR. Do not push directly to `main`, rebase a
20
+ shared branch, or squash a completed PR: the repository preserves merge-commit
21
+ history. See the repository's [full contribution guide](https://github.com/jbcom/game-harness/blob/main/CONTRIBUTING.md).
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Entry points
3
+ description: The full map of Game Harness subpath exports and their required peers.
4
+ ---
5
+
6
+ Import only the entry point a game uses:
7
+
8
+ - `game-harness/playwright` for Playwright projects and strict
9
+ preview-server configuration; install `@playwright/test`.
10
+ - `game-harness/silent-qa` for a peer-free,
11
+ audio-engine-agnostic application-side runtime mute adapter.
12
+ - `game-harness/production-runtime` for a fresh, silent
13
+ production-artifact or exact-live boot; install `@playwright/test`.
14
+ - `game-harness/chromium` for peer-free Chromium renderer and
15
+ silence launch profiles.
16
+ - `game-harness/vitest` for Vitest Browser Mode; install
17
+ `vitest` and `@vitest/browser-playwright`.
18
+ - `game-harness`, `/lighthouse`, `/release-ladder`, and
19
+ `/visual-battery` for peer-free verification utilities.
20
+
21
+ Framework peers are intentionally optional at install time and are never
22
+ loaded by the package root.
23
+
24
+ ## Package boundaries
25
+
26
+ | Entry point | Responsibility | Required peer |
27
+ | --------------------- | --------------------------------------------------------------- | -------------------------------------- |
28
+ | package root | Lighthouse policy, release ladder, visual battery | None |
29
+ | `/chromium` | Renderer and mandatory mute launch profile | None |
30
+ | `/silent-qa` | Application-side runtime mute request and readiness marker | None |
31
+ | `/playwright` | Device tiers, isolated ports, preview server, silent navigation | `@playwright/test` |
32
+ | `/production-runtime` | Fresh server/browser lifecycle and runtime evidence | `@playwright/test` |
33
+ | `/vitest` | Vitest Browser Mode configuration | `vitest`, `@vitest/browser-playwright` |
34
+ | `/lighthouse` | Immutable Lighthouse CI presets | None |
35
+ | `/release-ladder` | Ordered sync/async verification steps | None |
36
+ | `/visual-battery` | Deterministic screenshot baseline orchestration | None |
37
+
38
+ The production-runtime entry point depends on the Playwright entry point
39
+ because it composes `openSilentGame()` into a fresh-browser lifecycle; no
40
+ dependency points back toward the root. See the [Architecture reference](/game-harness/reference/architecture/)
41
+ for the complete module map and runtime-verification sequence.
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: Getting started
3
+ description: Install Game Harness and pick the entry points your game needs.
4
+ ---
5
+
6
+ Node 22 or newer is required. Install the package plus only the peer family
7
+ for the integration you use:
8
+
9
+ ```sh
10
+ # Playwright config and production-runtime verification
11
+ pnpm add -D game-harness @playwright/test
12
+ pnpm exec playwright install chromium
13
+
14
+ # Vitest Browser Mode instead
15
+ pnpm add -D game-harness vitest @vitest/browser-playwright playwright
16
+ pnpm exec playwright install chromium
17
+
18
+ # Peer-free Lighthouse, release-ladder, or visual-battery utilities
19
+ pnpm add -D game-harness
20
+ ```
21
+
22
+ Framework peers are intentionally optional at install time and are never
23
+ loaded by the package root. Each consumer installs only the peers required by
24
+ the framework entry point it imports — a Playwright-only game installs
25
+ `@playwright/test` but does not need Vitest or `@vitest/browser-playwright`.
26
+
27
+ ## Current release matrix
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
34
+ conformance matrix is Playwright 1.62.1 and Vitest Browser 4.1.10.
35
+ Package-boundary consumers run with a credential-free home directory and npm
36
+ configuration, install only the peer family needed by each entry point, and
37
+ exercise ESM, CommonJS, the CLI, silent runtime markers, and Chromium launch
38
+ profiles. `publint` and `@arethetypeswrong/cli` independently validate package
39
+ metadata and declarations.
40
+
41
+ Continue to [Quick start](/game-harness/quick-start/) to wire up the silent
42
+ runtime marker and your first Playwright config.
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: Chromium launch profile & Silent QA
3
+ description: The peer-free primitives behind every silent browser launch.
4
+ ---
5
+
6
+ ```ts
7
+ import { chromium } from '@playwright/test';
8
+ import { createChromiumLaunchProfile } from 'game-harness/chromium';
9
+
10
+ const { args, env } = createChromiumLaunchProfile({
11
+ gpuMode: process.platform === 'linux' && process.env.CI ? 'linux-hardware-vulkan' : 'auto',
12
+ });
13
+ const browser = await chromium.launch({ args, env, headless: false });
14
+ ```
15
+
16
+ `createChromiumLaunchProfile()` is the peer-free primitive behind both
17
+ `definePlaywrightConfig()` and `defineBrowserTestConfig()` — use it directly
18
+ when driving Chromium yourself (a custom launch script, `production-runtime`'s
19
+ own internals, or a non-Playwright automation layer). It resolves one of
20
+ three renderer policies (`auto` leaves selection to Chromium; `software`
21
+ opts into SwiftShader explicitly; `linux-hardware-vulkan` applies the
22
+ reviewed Mesa/ANGLE flags plus `EGL_PLATFORM=surfaceless` for a runner
23
+ exposing `/dev/dri/renderD128`) and always de-duplicates and appends
24
+ `--mute-audio` last, so a caller-supplied arg list can never accidentally
25
+ drop the silence guard.
26
+
27
+ Every browser launched by `definePlaywrightConfig()` or
28
+ `defineBrowserTestConfig()` receives Chromium's `--mute-audio` argument as a
29
+ defense-in-depth guard, including projects with custom launch options. The
30
+ application must also expose a non-persistent mute mode so tests fail closed
31
+ before interacting with it:
32
+
33
+ ```ts
34
+ import { activateSilentQa } from 'game-harness/silent-qa';
35
+ import { Howler } from 'howler';
36
+
37
+ // Evaluate before the rest of the application/audio graph.
38
+ export const silentQaActive = activateSilentQa(() => Howler.mute(true));
39
+ ```
40
+
41
+ `activateSilentQa()` detects `?muted` by presence, invokes the consumer-owned
42
+ audio-engine callback, and only then publishes
43
+ `<html data-audio-mode="muted-test">`. It never reads or writes saved audio
44
+ preferences. Consumers use its boolean return value or `isSilentQaActive()`
45
+ to prevent later preference restoration from overriding the page-lifetime
46
+ mute. If the audio engine mutes asynchronously, await
47
+ `activateSilentQaAsync()`; passing a promise-returning callback to the
48
+ synchronous function throws instead of publishing a premature readiness
49
+ marker.
50
+
51
+ ```ts
52
+ import { openSilentGame } from 'game-harness/playwright';
53
+
54
+ test('starts a game without audible QA', async ({ page }) => {
55
+ await openSilentGame(page, '/my-game/', { scenario: 'new-game' });
56
+ // The helper returns only after html[data-audio-mode="muted-test"] exists.
57
+ });
58
+ ```
59
+
60
+ `openSilentGame()` adds `?muted=1` (preserving other query parameters and the
61
+ fragment), navigates, and requires the application to set
62
+ `data-audio-mode="muted-test"` on `<html>`. The mute mode is runtime-only: it
63
+ must mute audio before any sound objects can play and must never write the
64
+ player's saved audio preference. Use `silentTestUrl()` when a test needs the
65
+ URL without navigating. Custom marker/query names are supported for legacy
66
+ adapters, but new games use these defaults. There is no audible-debug
67
+ exception: verify audio behavior through programmatic state, mocks, or
68
+ analyser assertions while agent-controlled playback remains muted.
69
+
70
+ Keep `reuseExistingServer` false for release evidence and assert the game
71
+ identity before exercising a journey. A process from another repository on a
72
+ familiar port must never be accepted as proof.
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: Lighthouse presets & release ladder
3
+ description: Immutable Lighthouse CI assertions and a thin ordered-step release orchestrator.
4
+ ---
5
+
6
+ ## Lighthouse CI presets
7
+
8
+ ```ts
9
+ // lighthouserc.mjs
10
+ import { lighthouseAssertions } from 'game-harness/lighthouse';
11
+
12
+ const config = lighthouseAssertions('game-default', {
13
+ url: ['http://localhost/index.html', 'http://localhost/settings/index.html'],
14
+ assertions: { 'categories:performance': ['warn', { minScore: 0.5 }] },
15
+ });
16
+
17
+ console.log(JSON.stringify(config, null, 2));
18
+ ```
19
+
20
+ `lighthouseAssertions()` returns a `lighthouserc.json`-shaped config object
21
+ for a named preset, with `overrides` merged on top (assertion overrides are
22
+ merged key-by-key on top of the preset's own; every other override field
23
+ replaces it wholesale). The only shipped preset, `'game-default'`, is a
24
+ production `lighthouserc.json` verbatim: performance/accessibility/best-practices
25
+ assertions at warn level (a score dip surfaces in CI logs without
26
+ hard-blocking a merge on Lighthouse's inherent run-to-run variance), with SEO
27
+ and PWA assertions off since these are single-page game shells with no SEO
28
+ surface and no installable-PWA requirement. To keep `lighthouserc.json` as
29
+ static JSON instead of a `.mjs` config, run this once locally and paste the
30
+ printed object — the factory has no runtime dependency on the consumer's
31
+ environment beyond the `overrides` you pass.
32
+
33
+ ## Release ladder orchestrator
34
+
35
+ ```ts
36
+ import { verifyReleaseLadder } from 'game-harness/release-ladder';
37
+ import { execSync } from 'node:child_process';
38
+
39
+ const result = await verifyReleaseLadder([
40
+ { name: 'lint', run: () => execSync('pnpm lint', { stdio: 'inherit' }) },
41
+ {
42
+ name: 'typecheck',
43
+ run: () => execSync('pnpm typecheck', { stdio: 'inherit' }),
44
+ },
45
+ { name: 'test', run: () => execSync('pnpm test', { stdio: 'inherit' }) },
46
+ { name: 'build', run: () => execSync('pnpm build', { stdio: 'inherit' }) },
47
+ ]);
48
+
49
+ process.exit(result.ok ? 0 : 1);
50
+ ```
51
+
52
+ `verifyReleaseLadder()` is a thin orchestrator for a `verify:*` release
53
+ ladder — an ordered list of named steps, each a plain sync or async function,
54
+ run in sequence and stopped at the first failure with a labeled summary. It
55
+ generalizes the pattern of many discrete `node scripts/verify-X.mjs` files
56
+ composed via a shell `&&` chain into one reusable primitive: a step can
57
+ inline its logic or delegate to an existing script via `execSync`. It never
58
+ throws — it returns a `ReleaseLadderResult` (`{ ok, ranSteps, failedStep?,
59
+ error? }`) so the caller decides how to report or exit;
60
+ `process.exit(result.ok ? 0 : 1)` is the CLI convention. Pass `{ log, error
61
+ }` to redirect the default `console.log`/`console.error` output (both
62
+ prefixed with `[verify]`).
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Playwright
3
+ description: Deterministic ports, strict-port preview servers, and silent Chromium launches.
4
+ ---
5
+
6
+ ```ts
7
+ import { definePlaywrightConfig } from 'game-harness/playwright';
8
+
9
+ export default definePlaywrightConfig({
10
+ port: 4391,
11
+ deviceTiers: ['desktop', 'mobile'],
12
+ gpuMode: process.platform === 'linux' && process.env.CI ? 'linux-hardware-vulkan' : 'auto',
13
+ webServerCommand: (port) =>
14
+ `pnpm build && pnpm exec vite preview --host 127.0.0.1 --port ${port} --strictPort`,
15
+ });
16
+ ```
17
+
18
+ The configured `port` is the stable local-development port. On GitHub or
19
+ Gitea Actions, the factory derives a deterministic port from repository, run,
20
+ and job identity so concurrent workflows cannot accidentally share the same
21
+ preview. Every Playwright config reload in the parent and worker processes
22
+ resolves the same value. `PLAYWRIGHT_PORT` or `PW_PORT` remains an exact
23
+ override. A rare cross-process/hash collision still fails closed because the
24
+ preview must use strict-port semantics; it never reuses a reachable process.
25
+
26
+ Playwright 1.62 forces coloured output in its web-server and worker children.
27
+ If the invoking shell exports `NO_COLOR`, Node warns that the variable is
28
+ ignored once Playwright adds `FORCE_COLOR=1`. During config evaluation the
29
+ factory therefore removes only the already-ignored `NO_COLOR` value from the
30
+ Playwright process environment before those children are spawned. This does
31
+ not modify the parent shell, and every other environment variable and
32
+ warning remains intact.
33
+
34
+ Use `webServerCommand(port)` when a consumer needs build or
35
+ asset-preparation steps around its preview. The callback receives the
36
+ already-resolved local or CI-isolated port. A literal
37
+ `overrides.webServer.command` remains supported, but a command that
38
+ hard-codes its own port bypasses isolation and is not valid release evidence.
39
+
40
+ Chromium is headed by default locally and in CI. Linux CI must provide a
41
+ display with `xvfb-run`; it should not change the browser to headless simply
42
+ to make WebGL start. The default `auto` profile lets Chromium select the
43
+ native renderer (including Metal on macOS). `software` is an explicit
44
+ SwiftShader fallback. `linux-hardware-vulkan` applies the reviewed
45
+ Mesa/ANGLE flags and `EGL_PLATFORM=surfaceless` for a runner that exposes
46
+ `/dev/dri/renderD128`.
47
+
48
+ A Gitea runner job using the hardware profile must install
49
+ `mesa-vulkan-drivers` and `xvfb`, require the render device with
50
+ `test -c /dev/dri/renderD128`, and run the browser command under
51
+ `xvfb-run --auto-servernum`. Use `requireHardwareWebGL()` in the journey
52
+ itself so a green test cannot silently fall back to SwiftShader or llvmpipe.
53
+ The assertion also fails closed when the browser withholds its unmasked
54
+ renderer.
55
+
56
+ `deviceTiers` declares the available matrix, while the default fast gate runs
57
+ only its first tier. Run `MULTIVIEW=1 pnpm exec playwright test` (or
58
+ `VISUAL=1 ...`) to include every declared tier.
@@ -0,0 +1,61 @@
1
+ ---
2
+ title: Production runtime verification
3
+ description: Turn a successful build into runtime evidence with a fresh, silent, fail-closed browser session.
4
+ ---
5
+
6
+ `verifyProductionRuntime()` turns a successful build into runtime evidence. It
7
+ launches a fresh Chromium process with `--mute-audio` and uses headed mode by
8
+ default,
9
+ navigates through `openSilentGame()`, and fails on page errors, console
10
+ errors, failed requests, HTTP errors, an inactive silent-QA marker, or a
11
+ changed local-storage sentinel. The required `assertReady` callback pins game
12
+ identity and the primary UI or canvas instead of accepting any app that
13
+ happens to answer on the same port.
14
+
15
+ ```ts
16
+ import {
17
+ findAvailableProductionPort,
18
+ requireHardwareWebGL,
19
+ verifyProductionRuntime,
20
+ } from 'game-harness/production-runtime';
21
+
22
+ const port = await findAvailableProductionPort();
23
+
24
+ await verifyProductionRuntime({
25
+ url: `http://127.0.0.1:${port}/`,
26
+ gpuMode: process.platform === 'linux' && process.env.CI ? 'linux-hardware-vulkan' : 'auto',
27
+ server: {
28
+ command: process.execPath,
29
+ args: [
30
+ 'node_modules/vite/bin/vite.js',
31
+ 'preview',
32
+ '--host=127.0.0.1',
33
+ `--port=${port}`,
34
+ '--strictPort',
35
+ ],
36
+ },
37
+ localStorageSentinels: { 'settings::muted': 'false' },
38
+ assertReady: async (page) => {
39
+ await page.getByRole('heading', { name: 'My Game' }).waitFor();
40
+ await page.locator('canvas').waitFor({ state: 'visible' });
41
+ const { renderer } = await requireHardwareWebGL(page);
42
+ console.log(`WebGL renderer: ${renderer}`);
43
+ },
44
+ });
45
+ ```
46
+
47
+ Pass `browserLaunchOptions: { headless: true }` only for a deliberately
48
+ constrained non-visual check. Release and final gameplay proof remain headed.
49
+
50
+ When `server` is present, its readiness URL must be unreachable before
51
+ launch; the verifier never reuses an arbitrary process. It owns that child
52
+ process, waits for readiness, and terminates it after either success or
53
+ failure. Omit `server` to apply the same strict gate to an already-deployed
54
+ exact-live URL. Every reachable readiness probe cancels its response body
55
+ after recording the status, including non-OK retry responses. A long polling
56
+ loop must not retain response streams or connections while it waits for the
57
+ owned server. Use `findAvailableProductionPort()` for CI or any shared runner
58
+ instead of a hard-coded port. It delegates selection to `get-port`, reserves
59
+ that selection against parallel calls in the current process, and still
60
+ requires the owned server to bind with strict-port semantics so an external
61
+ race fails closed.
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Visual battery
3
+ description: Deterministic, fail-closed screenshot baseline orchestration.
4
+ ---
5
+
6
+ Run the default harness directory in update mode, or enforce committed
7
+ baselines in CI:
8
+
9
+ ```sh
10
+ pnpm exec game-harness-visual-battery tests/harness
11
+ pnpm exec game-harness-visual-battery tests/harness --ci
12
+ ```
13
+
14
+ `test-harness-visual-battery` remains as a compatibility alias. Programmatic
15
+ callers can pass `testCommand: { command, args }`; the executable and
16
+ discovered harness paths are invoked directly rather than interpolated
17
+ through a shell.
18
+
19
+ `runVisualBattery()` owns one canonical `__screenshots__` tree directly under
20
+ the configured harness directory. Vitest screenshot paths are relative to the
21
+ test file, so a harness must write `__screenshots__/name.png`, not a
22
+ repository-relative path such as `tests/harness/__screenshots__/name.png`.
23
+ The battery rejects any second `__screenshots__` directory nested elsewhere
24
+ under the harness tree; otherwise an apparently green run could leave an
25
+ important screenshot outside the Git diff gate.
26
+
27
+ When two renderers cannot produce byte-identical PNGs, keep strict profiles
28
+ instead of adding a pixel threshold. Pass `baselineProfile: 'linux'`; the
29
+ battery compares `__screenshots__/linux/` and exposes the same value to Vite
30
+ as `VITE_VISUAL_BASELINE_PROFILE`. Screenshot helpers should include that
31
+ optional directory in their path:
32
+
33
+ ```ts
34
+ const profile = import.meta.env.VITE_VISUAL_BASELINE_PROFILE?.trim();
35
+ const path = profile ? `__screenshots__/${profile}/scene.png` : '__screenshots__/scene.png';
36
+ ```
37
+
38
+ `baselinesDir` names the baseline root. When it is combined with
39
+ `baselineProfile`, the profile is always appended below that root. For
40
+ example, `{ baselinesDir: 'visual-baselines', baselineProfile: 'linux' }`
41
+ owns and diffs `visual-baselines/linux/`.
42
+
43
+ For WebGL scenes, capture the canvas locator instead of the full browser
44
+ page:
45
+
46
+ ```ts
47
+ await page.locator('canvas').screenshot({ path: '__screenshots__/scene.png' });
48
+ ```
49
+
50
+ If a canvas baseline is stable alone but changes after other harnesses have
51
+ run in the same long-lived Chromium process, isolate that file so it gets a
52
+ fresh browser process while the remaining files stay in one fast batch:
53
+
54
+ ```ts
55
+ runVisualBattery('tests/harness', {
56
+ isolatedHarnessFiles: ['scene.browser.test.tsx'],
57
+ });
58
+ ```
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Vitest Browser Mode
3
+ description: A real-Chromium Vitest Browser Mode project, headed by default and silent at launch.
4
+ ---
5
+
6
+ ```ts
7
+ import { defineConfig } from 'vitest/config';
8
+ import { defineBrowserTestConfig } from 'game-harness/vitest';
9
+
10
+ export default defineConfig({
11
+ test: {
12
+ projects: [
13
+ {
14
+ extends: true,
15
+ test: { name: 'unit', environment: 'node', include: ['tests/unit/**'] },
16
+ },
17
+ {
18
+ extends: true,
19
+ test: defineBrowserTestConfig({
20
+ optimizeDeps: ['three/examples/jsm/utils/SkeletonUtils.js'],
21
+ }),
22
+ },
23
+ ],
24
+ },
25
+ });
26
+ ```
27
+
28
+ `defineBrowserTestConfig()` builds the `test` fragment for a real-Chromium
29
+ Vitest Browser Mode project: headed by default (both locally and in CI, same
30
+ as `definePlaywrightConfig()`), silent at the Chromium launch boundary, and a
31
+ fixed viewport with `deviceScaleFactor: 1` so a headed macOS run never
32
+ recaptures visual baselines at the host's Retina scale. Pass `headless:
33
+ 'ci-only'` only for a hosted runner genuinely without a display; CI should
34
+ normally keep the headed default and run under `xvfb-run`. Requires `vitest`
35
+ and `@vitest/browser-playwright` installed as peers.
36
+
37
+ `optimizeDeps` surfaces module specifiers (deep imports Vite's scanner won't
38
+ discover on its own) that must also be merged into your top-level
39
+ `vite.config.ts`'s `optimizeDeps.include` — read them off the returned
40
+ fragment's `__optimizeDepsInclude` field, since Vitest's `test` block has no
41
+ `optimizeDeps` field of its own.
@@ -0,0 +1,29 @@
1
+ ---
2
+ title: Game Harness
3
+ description: Release-grade browser QA primitives for TypeScript games.
4
+ ---
5
+
6
+ Game Harness turns a successful build into evidence: silent browser sessions,
7
+ deterministic device tiers, byte-exact screenshots, production-runtime proof,
8
+ and Lighthouse gates.
9
+
10
+ It is intentionally a focused library rather than a test framework. Your game
11
+ keeps its own journeys, assertions, art direction, and audio engine; Game
12
+ Harness supplies the reusable safety and orchestration layer around Playwright
13
+ and Vitest Browser Mode.
14
+
15
+ ## Why Game Harness
16
+
17
+ - **Silent by construction.** Chromium launches with `--mute-audio`, and tests
18
+ wait for an application-owned runtime mute marker before interacting.
19
+ - **No accidental server reuse.** CI ports are deterministic per job, preview
20
+ commands use strict-port semantics, and production verification refuses an
21
+ already-reachable readiness URL.
22
+ - **Visual evidence that fails closed.** Screenshot baselines are scoped,
23
+ profile-aware, and checked through Git without fuzzy thresholds or shell
24
+ interpolation.
25
+ - **Real package boundaries.** Framework peers stay optional and isolated to
26
+ subpath exports, with clean ESM, CommonJS, type, CLI, and install smoke tests.
27
+
28
+ Start with [Getting started](getting-started/) to choose the integration your
29
+ game needs.
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "game-harness-docs",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "sourcey dev",
7
+ "build": "sourcey build",
8
+ "validate": "sourcey build"
9
+ },
10
+ "dependencies": {
11
+ "sourcey": "^3.6.0"
12
+ }
13
+ }
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: Quick start
3
+ description: Wire up the silent runtime marker and your first Playwright config.
4
+ ---
5
+
6
+ Activate the runtime-only mute before the application restores saved audio
7
+ preferences or creates anything that can play sound:
8
+
9
+ ```ts
10
+ // src/silent-qa.ts
11
+ import { activateSilentQa } from 'game-harness/silent-qa';
12
+ import { Howler } from 'howler';
13
+
14
+ export const silentQaActive = activateSilentQa(() => Howler.mute(true));
15
+ ```
16
+
17
+ Then use the shared Playwright config and open the game through the
18
+ fail-closed navigation helper:
19
+
20
+ ```ts
21
+ // playwright.config.ts
22
+ import { definePlaywrightConfig } from 'game-harness/playwright';
23
+
24
+ export default definePlaywrightConfig({
25
+ port: 4391,
26
+ webServerCommand: (port) =>
27
+ `pnpm build && pnpm exec vite preview --host 127.0.0.1 --port ${port} --strictPort`,
28
+ });
29
+ ```
30
+
31
+ ```ts
32
+ // tests/e2e/boot.spec.ts
33
+ import { expect, test } from '@playwright/test';
34
+ import { openSilentGame } from 'game-harness/playwright';
35
+
36
+ test('boots the intended game silently', async ({ page }) => {
37
+ await openSilentGame(page, '/my-game/', { scenario: 'new-game' });
38
+ await expect(page.getByRole('heading', { name: 'My Game' })).toBeVisible();
39
+ await expect(page.locator('canvas')).toBeVisible();
40
+ });
41
+ ```
42
+
43
+ The application marker is part of the contract: `openSilentGame()` does not
44
+ return until `<html data-audio-mode="muted-test">` exists.
45
+
46
+ Continue to [Entry points](/game-harness/entry-points/) for the full map of
47
+ subpath exports, or jump straight to the [Playwright guide](/game-harness/guides/playwright/).
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: Troubleshooting
3
+ description: Common failure modes and their causes.
4
+ ---
5
+
6
+ - **A headed browser cannot start on Linux CI:** install Chromium
7
+ dependencies and run the browser command with `xvfb-run --auto-servernum`;
8
+ do not silently switch release evidence to headless mode.
9
+ - **`PLAYWRIGHT_PORT` or `PW_PORT` is rejected:** overrides must be integer
10
+ TCP ports from 1 through 65535. Invalid explicit values fail instead of
11
+ falling back to another port.
12
+ - **The visual battery reports zero PNGs:** each harness must write into its
13
+ canonical `__screenshots__` directory. An existing but empty directory is
14
+ not release evidence.
15
+ - **`test:package` reports an npm version too old:** the package-boundary test
16
+ requires npm 10 or newer (bundled with every Node version in the supported
17
+ `>=22` range); use the Node version from `.nvmrc` or any newer supported
18
+ LTS.
19
+ - **A local Chrome channel is unavailable:** leave `PW_CHROMIUM_CHANNEL`
20
+ unset on CI to use Playwright's bundled Chromium, or set it explicitly to
21
+ an installed supported channel for a local branded-browser run.
22
+
23
+ ## Releases and support
24
+
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.
28
+ Changes are recorded in the [CHANGELOG](https://github.com/jbcom/game-harness/blob/main/CHANGELOG.md).
29
+
30
+ Report defects and feature requests through the
31
+ [repository issue forms](https://github.com/jbcom/game-harness/issues).
32
+ Report vulnerabilities privately as described in
33
+ [SECURITY.md](https://github.com/jbcom/game-harness/blob/main/SECURITY.md).
@@ -0,0 +1,15 @@
1
+ ---
2
+ title: Security
3
+ description: Report vulnerabilities privately and understand the project's security boundary.
4
+ ---
5
+
6
+ Do not report security issues in a public issue. Use [GitHub private security
7
+ advisories](https://github.com/jbcom/game-harness/security/advisories/new) so a
8
+ fix can be prepared before disclosure. The latest minor release receives
9
+ security fixes.
10
+
11
+ For the complete reporting policy, see [SECURITY.md](https://github.com/jbcom/game-harness/blob/main/SECURITY.md).
12
+
13
+ Every pull request must pass the dependency-review and repository-policy checks
14
+ before it can merge. The repository-policy check treats changes from external
15
+ forks as untrusted and rejects edits to the repository control plane.