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.
- package/AGENTS.md +140 -0
- package/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +568 -0
- package/bin/test-harness-visual-battery.mjs +6 -0
- package/dist/cjs/bin/visual-battery.js +43 -0
- package/dist/cjs/browser-config.js +83 -0
- package/dist/cjs/chromium-launch.js +37 -0
- package/dist/cjs/index.js +10 -0
- package/dist/cjs/lighthouse.js +73 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/playwright-config.js +303 -0
- package/dist/cjs/production-runtime.js +315 -0
- package/dist/cjs/release-ladder.js +40 -0
- package/dist/cjs/silent-qa.js +75 -0
- package/dist/cjs/visual-battery.js +247 -0
- package/dist/esm/bin/visual-battery.js +41 -0
- package/dist/esm/browser-config.js +80 -0
- package/dist/esm/chromium-launch.js +34 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/lighthouse.js +70 -0
- package/dist/esm/playwright-config.js +292 -0
- package/dist/esm/production-runtime.js +304 -0
- package/dist/esm/release-ladder.js +37 -0
- package/dist/esm/silent-qa.js +67 -0
- package/dist/esm/visual-battery.js +239 -0
- package/dist/types/bin/visual-battery.d.cts +2 -0
- package/dist/types/bin/visual-battery.d.ts +2 -0
- package/dist/types/browser-config.d.cts +100 -0
- package/dist/types/browser-config.d.ts +100 -0
- package/dist/types/chromium-launch.d.cts +27 -0
- package/dist/types/chromium-launch.d.ts +27 -0
- package/dist/types/index.d.cts +3 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/lighthouse.d.cts +44 -0
- package/dist/types/lighthouse.d.ts +44 -0
- package/dist/types/playwright-config.d.cts +122 -0
- package/dist/types/playwright-config.d.ts +122 -0
- package/dist/types/production-runtime.d.cts +105 -0
- package/dist/types/production-runtime.d.ts +105 -0
- package/dist/types/release-ladder.d.cts +38 -0
- package/dist/types/release-ladder.d.ts +38 -0
- package/dist/types/silent-qa.d.cts +47 -0
- package/dist/types/silent-qa.d.ts +47 -0
- package/dist/types/visual-battery.d.cts +69 -0
- package/dist/types/visual-battery.d.ts +69 -0
- package/docs/404.md +17 -0
- package/docs/agent-guide.md +10 -0
- package/docs/architecture.md +81 -0
- package/docs/assets/game-harness-hero.webp +0 -0
- package/docs/changelog.md +9 -0
- package/docs/contributing.md +21 -0
- package/docs/entry-points.md +41 -0
- package/docs/getting-started.md +42 -0
- package/docs/guides/chromium-and-silent-qa.md +72 -0
- package/docs/guides/lighthouse-and-release-ladder.md +62 -0
- package/docs/guides/playwright.md +58 -0
- package/docs/guides/production-runtime.md +61 -0
- package/docs/guides/visual-battery.md +58 -0
- package/docs/guides/vitest.md +41 -0
- package/docs/introduction.md +29 -0
- package/docs/package.json +13 -0
- package/docs/quick-start.md +47 -0
- package/docs/reference/troubleshooting.md +33 -0
- package/docs/security.md +15 -0
- package/docs/sourcey.config.ts +79 -0
- package/llms.txt +32 -0
- package/maestro/smoke.template.yaml +11 -0
- 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,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).
|
package/docs/security.md
ADDED
|
@@ -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.
|