@roku-sdk/device-test-client 0.0.0-stage → 2.19.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 (61) hide show
  1. package/BREAKING_CHANGES.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -2
  4. package/docs/baselines.md +221 -0
  5. package/docs/configuration.md +258 -0
  6. package/docs/dom-assertions.md +264 -0
  7. package/docs/errors.md +544 -0
  8. package/docs/examples/deep-link-cold-start.md +85 -0
  9. package/docs/examples/focus-navigation.md +81 -0
  10. package/docs/examples/log-driven-assertion.md +99 -0
  11. package/docs/examples/precondition-skips.md +150 -0
  12. package/docs/examples/screenshot-regression.md +103 -0
  13. package/docs/examples/smoke-test.md +69 -0
  14. package/docs/getting-started.md +195 -0
  15. package/docs/jest-integration.md +331 -0
  16. package/docs/log-assertions.md +349 -0
  17. package/docs/preconditions.md +317 -0
  18. package/docs/rkclient.md +657 -0
  19. package/docs/screenshot-assertions.md +316 -0
  20. package/package.json +81 -3
  21. package/src/assertions/assertions.ts +661 -0
  22. package/src/assertions/baseline-env-guard.ts +43 -0
  23. package/src/assertions/baselines.ts +178 -0
  24. package/src/assertions/screenshot-artifacts.ts +39 -0
  25. package/src/assertions/screenshot-diff.ts +447 -0
  26. package/src/client/build-cache.ts +292 -0
  27. package/src/client/client.ts +1964 -0
  28. package/src/client/deploy.ts +407 -0
  29. package/src/client/ecp-queue.ts +112 -0
  30. package/src/client/ecp-retry.ts +94 -0
  31. package/src/client/factory.ts +170 -0
  32. package/src/client/health.ts +345 -0
  33. package/src/client/parsers.ts +162 -0
  34. package/src/client/server-url.ts +137 -0
  35. package/src/constants.ts +520 -0
  36. package/src/device/device-class.ts +57 -0
  37. package/src/device/device-env-sync.ts +58 -0
  38. package/src/device/device-resolver-sync.ts +43 -0
  39. package/src/device/device-resolver.ts +141 -0
  40. package/src/device/lease-provisioning.ts +54 -0
  41. package/src/errors.ts +774 -0
  42. package/src/index.ts +181 -0
  43. package/src/internal/constants.ts +3 -0
  44. package/src/jest/artifacts.ts +87 -0
  45. package/src/jest/device-test-environment.ts +420 -0
  46. package/src/jest/failure-artifacts.ts +169 -0
  47. package/src/jest/global-setup.ts +137 -0
  48. package/src/jest/global-teardown.ts +132 -0
  49. package/src/jest/install-hooks.ts +490 -0
  50. package/src/jest/jest-setup.ts +53 -0
  51. package/src/jest/preconditions.ts +239 -0
  52. package/src/jest/telemetry.ts +331 -0
  53. package/src/logs/device-log-sink.ts +69 -0
  54. package/src/logs/log-buffer.ts +278 -0
  55. package/src/logs/log-session.ts +436 -0
  56. package/src/logs/log-stream-client.ts +296 -0
  57. package/src/paths.ts +65 -0
  58. package/src/types.ts +472 -0
  59. package/support/constants.ts +17 -0
  60. package/support/deploy.ts +27 -0
  61. package/support/paths.ts +13 -0
@@ -0,0 +1,17 @@
1
+ # Breaking Changes
2
+
3
+ ## `./internal/*` subpaths removed
4
+
5
+ The `./internal/*` subpaths are removed. The harness helpers that were reachable only through `./internal/deploy`, `./internal/constants`
6
+ and `./internal/paths` have documented entry points with the same contents:
7
+
8
+ | Before | After |
9
+ |--------|-------|
10
+ | `@roku-sdk/device-test-client/internal/deploy` | `@roku-sdk/device-test-client/support/deploy` |
11
+ | `@roku-sdk/device-test-client/internal/constants` | `@roku-sdk/device-test-client/support/constants` |
12
+ | `@roku-sdk/device-test-client/internal/paths` | `@roku-sdk/device-test-client/support/paths` |
13
+
14
+ `ARTIFACT_FILE` is also exported from the package root.
15
+
16
+ **Migration.** Replace `/internal/` with `/support/` in these imports. The `support/constants`
17
+ and `support/paths` entry points stay safe to import from a CommonJS-mode `jest.config.ts`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roku, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,145 @@
1
- # Temporary Holding Version
1
+ # @roku-sdk/device-test-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Single entry-point SDK for writing **integration tests that drive real Roku
4
+ devices** from a Jest suite running on your host. The package bundles:
5
+
6
+ - a queued, rate-limited **ECP client** (`RkClient`) that talks to the device,
7
+ - **build + sideload + launch** primitives with readiness and health checks,
8
+ - **DOM, screenshot, and log assertion** pillars with baseline workflows,
9
+ - **precondition-aware `it`/`describe` wrappers** that skip on firmware /
10
+ model / resolution mismatches,
11
+ - first-class **Jest wiring** (`globalSetup`, `globalTeardown`,
12
+ `installDeviceTestHooks`, a custom test environment) that captures rich
13
+ per-failure diagnostic bundles out of the box.
14
+
15
+ Everything is a named export of the package root — there is no deep-path
16
+ import surface to remember.
17
+
18
+ ```ts
19
+ import {
20
+ createDefaultClient,
21
+ installDeviceTestHooks,
22
+ itOnDevice,
23
+ expectTree,
24
+ expectScreenshot,
25
+ findNode
26
+ } from "@roku-sdk/device-test-client";
27
+ ```
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pnpm add -D @roku-sdk/device-test-client
33
+ # or
34
+ npm install -D @roku-sdk/device-test-client
35
+ ```
36
+
37
+ Requires **Node 24+** (for global `fetch`), **Jest 29+**, and the
38
+ `[rk](../tooling)` CLI/daemon checked in alongside this package. Log
39
+ assertions need `rk server` running on the host.
40
+
41
+ ## 60-second quick start
42
+
43
+ ```ts
44
+ import {
45
+ createDefaultClient,
46
+ installDeviceTestHooks
47
+ } from "@roku-sdk/device-test-client";
48
+
49
+ const client = createDefaultClient();
50
+ installDeviceTestHooks({ client });
51
+
52
+ test("app launches and shows the home scene", async () => {
53
+ await client.buildAndDeploy("apps/buttons", {
54
+ readyNode: { attr: { id: "home-title", text: "Welcome" } }
55
+ });
56
+ await client.keypress("Select");
57
+ await client.waitForNode(
58
+ { focused: true, attr: { id: "first-button" } },
59
+ { timeout: 3_000 }
60
+ );
61
+ });
62
+ ```
63
+
64
+ For a full Jest skeleton (config + setup files) jump to
65
+ `[docs/getting-started.md](docs/getting-started.md)`.
66
+
67
+ ## Documentation
68
+
69
+ The `docs/` folder is the manual. Each page opens with a short summary so
70
+ you can skim the table of contents; every documented symbol ships with its
71
+ TypeScript signature **and** a runnable example.
72
+
73
+
74
+ | Topic | What's inside |
75
+ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
76
+ | [Getting started](docs/getting-started.md) | Prerequisites, install, first test, Jest config skeleton |
77
+ | `[RkClient` reference](docs/rkclient.md) | Every method, every option type, rate limits, retries |
78
+ | [DOM assertions](docs/dom-assertions.md) | `findNode`, `expectFocused`, `waitForNode`, tree baselines |
79
+ | [Screenshot assertions](docs/screenshot-assertions.md) | `captureStableScreenshot`, `expectScreenshot`, masks, thresholds, alignment tolerance |
80
+ | [Log assertions](docs/log-assertions.md) | `logPosition`, `waitForLog`, `expectNoLog`, gap handling |
81
+ | [Preconditions](docs/preconditions.md) | `itOnDevice` / `describeOnDevice` / `requirePrecondition`, device-class tiering |
82
+ | [Jest integration](docs/jest-integration.md) | `globalSetup` / `globalTeardown`, `installDeviceTestHooks`, custom test environment, failure bundles |
83
+ | [Baselines](docs/baselines.md) | `hd/` + `fhd/` routing, `UPDATE_BASELINES=1` workflow, CI guard |
84
+ | [Configuration](docs/configuration.md) | Env vars, tunable constants, `test-results/` artifact layout |
85
+ | [Error reference](docs/errors.md) | Every exported error class with fields and recovery advice |
86
+
87
+
88
+ Worked examples that cross feature boundaries live under
89
+ `[docs/examples/](docs/examples/)`:
90
+
91
+ - `[smoke-test](docs/examples/smoke-test.md)` — shortest possible deploy + one assertion
92
+ - `[deep-link-cold-start](docs/examples/deep-link-cold-start.md)` — reliable cold-sideload deep-link delivery
93
+ - `[focus-navigation](docs/examples/focus-navigation.md)` — `keysequence` + focus-change polling
94
+ - `[screenshot-regression](docs/examples/screenshot-regression.md)` — stable capture + masked `expectScreenshot`
95
+ - `[log-driven-assertion](docs/examples/log-driven-assertion.md)` — `logPosition` → `keypress` → `waitForLog`
96
+ - `[precondition-skips](docs/examples/precondition-skips.md)` — skipping by firmware / model / resolution
97
+
98
+ ## Feature matrix
99
+
100
+ - **ECP queries** — `queryAppUi`, `queryDeviceInfo` (+ cache),
101
+ `queryActiveApp`, `captureScreenshot`
102
+ - **Input** — `keypress`, `keysequence` with validated key names and an
103
+ enforced inter-key delay
104
+ - **Lifecycle** — `launch`, `buildAndDeploy`, `buildAndLaunchWithParams`
105
+ (cold sideload + reliable deep-link delivery)
106
+ - **Readiness & health** — semantic readiness gate via `readyNode`, system
107
+ dialog detection, per-worker health flag with fail-fast `beforeEach`
108
+ - **Polling** — `waitForNode`, `captureStableScreenshot` with crash-probe
109
+ on every iteration
110
+ - **DOM assertions** — `findNode` / `findAllNodes` / `findFocusedNode` /
111
+ `expectFocused`, plus baseline comparison via
112
+ `expectTree(...).toMatchBaseline(...)`
113
+ - **Screenshot assertions** — `expectScreenshot(...).toMatchBaseline(...)`
114
+ with pixelmatch, mask regions, decoupled aggregate + per-pixel thresholds,
115
+ and opt-in alignment tolerance for firmware-drawn surfaces that re-center
116
+ between devices
117
+ - **Log assertions** — `logPosition`, `waitForLog`, `expectNoLog`, bulk
118
+ `logs`, gap-aware semantics
119
+ - **Preconditions** — firmware semver ranges, videoMode prefix, exact
120
+ modelFamily / locale / country, derived device-class tiering
121
+ - **Failure diagnostics** — per-failure bundles under
122
+ `test-results/failures/<sanitized>/` (screenshot, DOM tree, recent logs,
123
+ error) and a streaming `test-results/device.log`
124
+
125
+ ## Compatibility
126
+
127
+ - **Runtime:** Node 24+ (global `fetch`).
128
+ - **Test runner:** Jest 29 or 30 (the custom environment `require()`s
129
+ `jest-environment-node` at runtime so either works).
130
+ - **Roku firmware:** any firmware that serves the standard ECP surface
131
+ (`/query/app-ui`, `/query/device-info`, `/query/active-app`,
132
+ `/launch/…`, `/keypress/…`, `/capture-screen/png`).
133
+ - **Videomodes:** `720p`, `720p60`, `1080p`, `1080p60`. See
134
+ `[SUPPORTED_VIDEO_MODES](docs/baselines.md#supported-video-modes)`.
135
+
136
+ ## Versioning
137
+
138
+ The package follows semver. The public surface is everything exported by
139
+ `@roku-sdk/device-test-client` (see `[src/index.ts](src/index.ts)`) — no
140
+ other import paths are supported. Renames of public symbols are major
141
+ bumps; additions are minor.
142
+
143
+ ## License
144
+
145
+ See the repository root.
@@ -0,0 +1,221 @@
1
+ # Baselines
2
+
3
+ Both [DOM](dom-assertions.md) and [screenshot](screenshot-assertions.md)
4
+ assertions read ground-truth artifacts from a **baselines directory**
5
+ routed by the device's current **videoMode**. This page documents the
6
+ directory layout, the resolution-routing rules, how to create or
7
+ update baselines via `UPDATE_BASELINES=1`, and the CI guard that
8
+ prevents accidental mutation.
9
+
10
+ - [Directory layout](#directory-layout)
11
+ - [Wiring via `installDeviceTestHooks`](#wiring-via-installdevicetesthooks)
12
+ - [Resolution routing](#resolution-routing)
13
+ - `[DEVICE_TEST_RESOLUTION` override](#device_test_resolution-override)
14
+ - [Supported videoModes](#supported-videomodes)
15
+ - `[UPDATE_BASELINES=1` workflow](#update_baselines1-workflow)
16
+ - [CI guard](#ci-guard)
17
+ - [Errors thrown](#errors-thrown)
18
+
19
+ ## Directory layout
20
+
21
+ ```
22
+ baselines/
23
+ ├── hd/ # 720p devices
24
+ │ └── grid/
25
+ │ ├── initial-tree.json
26
+ │ └── initial.png
27
+ └── fhd/ # 1080p devices
28
+ └── grid/
29
+ ├── initial-tree.json
30
+ └── initial.png
31
+ ```
32
+
33
+ Every assertion call with a relative baseline path (e.g.
34
+ `"grid/initial-tree.json"`) is rewritten to
35
+ `<baselineRoot>/<hd|fhd>/<relPath>` at runtime. The relative path is
36
+ identical across resolutions — only the subdir differs.
37
+
38
+ ## Wiring via `installDeviceTestHooks`
39
+
40
+ In v2.0.0 both the baselines root and the "which client owns the
41
+ active `videoMode`" signal are set through `installDeviceTestHooks`:
42
+
43
+ ```ts
44
+ import {
45
+ createDefaultClient,
46
+ installDeviceTestHooks
47
+ } from "@roku-sdk/device-test-client";
48
+ import { resolve } from "node:path";
49
+
50
+ const client = createDefaultClient();
51
+
52
+ installDeviceTestHooks({
53
+ client, // → active-client signal
54
+ baselineRoot: resolve(__dirname, "../baselines") // → baseline root
55
+ });
56
+ ```
57
+
58
+ Behind the scenes these map to the internal `setActiveClient` /
59
+ `setBaselineRoot` module-level singletons that previous versions
60
+ asked consumers to call directly; centralising the wiring in one
61
+ options-object call removes a whole class of "forgot to register the
62
+ client" bugs.
63
+
64
+ **Precedence for the baseline root** (checked in order):
65
+
66
+ 1. `installDeviceTestHooks({ baselineRoot })` value.
67
+ 2. `DEVICE_TEST_BASELINES` env var.
68
+ 3. `<process.cwd()>/baselines`.
69
+
70
+ > **Internal / deep-import.** The raw `setBaselineRoot(absPath)` and
71
+ > `setActiveClient(client | null)` functions still exist inside the
72
+ > package but are no longer re-exported from the barrel. The tiny
73
+ > number of out-of-band consumers (ad-hoc reporters, tests of this
74
+ > package's own internals) can still import them via
75
+ > `@roku-sdk/device-test-client/src/assertions/baselines`, but deep
76
+ > paths are **not** covered by the SemVer contract — prefer
77
+ > `installDeviceTestHooks` in normal test code.
78
+
79
+ ## Resolution routing
80
+
81
+ ```ts
82
+ type BaselineResolution = "hd" | "fhd";
83
+
84
+ function videoModeToResolution(videoMode: string): BaselineResolution | null;
85
+ ```
86
+
87
+ Resolution is picked with the following precedence:
88
+
89
+ 1. **Active client registered + videoMode recognised** — map to the
90
+ corresponding subdir. The env override is intentionally **not**
91
+ consulted in this path.
92
+ 2. **Active client registered + videoMode unrecognised** — throw
93
+ `BaselineResolutionError({ reason: "unsupported-videoMode" })`.
94
+ The client is authoritative; silently falling back would paper
95
+ over a misconfigured fixture or unexpected firmware mode.
96
+ 3. **No active client** — consult `DEVICE_TEST_RESOLUTION`:
97
+ - `"hd"` or `"fhd"` → use it.
98
+ - any other value → `invalid-env-value`.
99
+ - unset → `no-active-client-and-no-env`.
100
+
101
+ `videoModeToResolution` lives alongside the internal
102
+ `setActiveClient` / `setBaselineRoot` helpers on the same deep-import
103
+ module (`@roku-sdk/device-test-client/src/assertions/baselines`) — it
104
+ is not part of the v2.0.0 barrel.
105
+
106
+ ## `DEVICE_TEST_RESOLUTION` override
107
+
108
+ ```bash
109
+ DEVICE_TEST_RESOLUTION=fhd pnpm jest path/to/file.test.ts
110
+ ```
111
+
112
+ Only consulted when no client is registered. Useful for ad-hoc
113
+ debugging when running a single test before `installDeviceTestHooks`
114
+ has a chance to wire the client — e.g. a throwaway reporter that
115
+ constructs its own assertion loop without the full setup.
116
+
117
+ ## Supported videoModes
118
+
119
+ ```ts
120
+ const SUPPORTED_VIDEO_MODES: ReadonlySet<string>;
121
+ ```
122
+
123
+ The SDK ships with:
124
+
125
+ - `720p` / `720p60` → `hd`
126
+ - `1080p` / `1080p60` → `fhd`
127
+
128
+ `assertSupportedVideoMode` (from
129
+ [Jest integration](jest-integration.md#artifact-helpers)) rejects
130
+ everything else in `createGlobalSetup` so misconfigured fixtures
131
+ fail before any baseline write.
132
+
133
+ ## `UPDATE_BASELINES=1` workflow
134
+
135
+ First-run (create baselines) and intentional-update flows share the
136
+ same toggle:
137
+
138
+ ```bash
139
+ UPDATE_BASELINES=1 pnpm jest path/to/file.test.ts
140
+ ```
141
+
142
+ **Behaviour per assertion:**
143
+
144
+
145
+ | Baseline state | Without flag | With flag |
146
+ | --------------------------------------- | --------------------------------------------------------- | ----------------------------------------------- |
147
+ | Missing | `BaselineMissingError` / `ScreenshotBaselineMissingError` | Baseline created atomically; assertion passes |
148
+ | Exists, matches | passes | passes (no write) |
149
+ | Exists, mismatch (tree) | `TreeMismatchError` | Overwritten atomically; assertion passes |
150
+ | Exists, mismatch (screenshot) | `ScreenshotMismatchError` + diff PNG | Overwritten atomically; **no diff PNG written** |
151
+ | Exists, dimension mismatch (screenshot) | `ScreenshotDimensionsMismatchError` | Overwritten atomically; assertion passes |
152
+
153
+
154
+ **Atomic writes.** Every baseline write goes through a per-pid +
155
+ random-suffix tmp file + `renameSync`, so readers always observe
156
+ either the old or new contents, never a half-written file.
157
+
158
+ **Accepted truthy spellings** (case-insensitive): `1`, `true`, `yes`.
159
+
160
+ **Workflow.**
161
+
162
+ ```bash
163
+ # 1. Make your UI change.
164
+ # 2. Refresh baselines locally.
165
+ UPDATE_BASELINES=1 pnpm jest
166
+
167
+ # 3. Review the git diff on the baselines directory.
168
+ git diff baselines/
169
+
170
+ # 4. Commit the updated baselines alongside the source change.
171
+ ```
172
+
173
+ ## CI guard
174
+
175
+ ```ts
176
+ function assertBaselineUpdatesAllowed(): void;
177
+ function isCi(): boolean;
178
+ ```
179
+
180
+ `assertBaselineUpdatesAllowed` throws when `CI` **and**
181
+ `UPDATE_BASELINES` are both set to a truthy value. It runs at the
182
+ top of `createGlobalSetup` **and** inline before every baseline
183
+ write, so an out-of-band `expectTree`/`expectScreenshot` call cannot
184
+ sneak past the `globalSetup` check.
185
+
186
+ `isCi()` and `assertBaselineUpdatesAllowed()` are internal helpers
187
+ of the baseline-write path; they are not exported from the v2.0.0
188
+ barrel. Consumers needing a parallel CI check should read
189
+ `process.env.CI` directly or import from
190
+ `@roku-sdk/device-test-client/src/assertions/baselines` as a
191
+ deep-import (SemVer-uncovered).
192
+
193
+ The error message cites `UPDATE_BASELINES=1` as the canonical
194
+ remediation regardless of the spelling that tripped the guard — if
195
+ you see it, drop the variable entirely, don't try a different
196
+ casing.
197
+
198
+ ## Errors thrown
199
+
200
+ - `BaselineResolutionError` — resolution could not be picked.
201
+ Fields: `videoMode` (when applicable), `reason` (one of
202
+ `"unsupported-videoMode"`, `"no-active-client-and-no-env"`,
203
+ `"invalid-env-value"`).
204
+ - `BaselineMissingError` — DOM baseline missing. Fields:
205
+ `baselinePath`, `resolution`, `videoMode`.
206
+ - `ScreenshotBaselineMissingError` — screenshot baseline missing.
207
+ Fields: `baselinePath`, `resolution`.
208
+ - `TreeMismatchError` — DOM baseline diff. Fields: `baselinePath`,
209
+ `firstDiff`, `expectedSnippet`, `actualSnippet`.
210
+ - `ScreenshotMismatchError` — screenshot diff over threshold.
211
+ Fields: `baselinePath`, `resolution`, `actual.w/h`,
212
+ `mismatchedPixels`, `totalPixels`, `ratio`, `threshold`,
213
+ `diffPath`.
214
+ - `ScreenshotDimensionsMismatchError` — canvas size diverged from
215
+ baseline. Fields: `baselinePath`, `resolution`, `baseline.w/h`,
216
+ `actual.w/h`.
217
+ - `Error` — raw from `assertBaselineUpdatesAllowed` in CI with
218
+ `UPDATE_BASELINES` set.
219
+
220
+ Full field lists and recovery advice in
221
+ `[errors.md](errors.md)`.
@@ -0,0 +1,258 @@
1
+ # Configuration
2
+
3
+ All behaviour users typically need to tune is driven by environment
4
+ variables or passed via `RkClientOptions`. The rest of this page
5
+ lists every knob the package exposes plus the on-disk artifact layout
6
+ it writes under `test-results/`.
7
+
8
+ - [Environment variables](#environment-variables)
9
+ - [Tunable constants](#tunable-constants)
10
+ - `[test-results/` layout](#test-results-layout)
11
+
12
+ ## Environment variables
13
+
14
+
15
+ | Variable | Type | Default | Purpose |
16
+ | ------------------------ | ---------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
17
+ | `RK_DEVICE_IP` | IPv4 string | — | Explicit override for the target Roku device. When set, bypasses `.roku/` active-device resolution. Consumed by the package's internal IP-resolver and `createDefaultClient`. See [Device credentials](#device-credentials) for the deploy-path contract. |
18
+ | `RK_DEVICE_PASSWORD` | string | — | Developer-mode password for the device named by `RK_DEVICE_IP`. **Must be set together with `RK_DEVICE_IP`** — a partial pair fails the deploy with `DeployFailedError({ phase: "config" })`. See [Device credentials](#device-credentials). |
19
+ | `RK_SERVER_URL` | `http://host:port[/api]` | `http://localhost:3737/api` | `rk server` base URL. Resolution precedence: `RkClientOptions.serverUrl` → `RK_SERVER_URL` → `.roku/rk-server.json` → default. Normalized to end with `/api` (and no trailing slash) regardless of source — every server route is mounted under `/api/...`, so a bare `http://host:port` is auto-promoted to the canonical shape. |
20
+ | `UPDATE_BASELINES` | `1` / `true` / `yes` (case-insens) | unset | Re-run writes baselines instead of diffing. Blocked by the CI guard when `CI` is also truthy. |
21
+ | `CI` | `1` / `true` / `yes` (case-insens) | unset | Marks the current run as CI. Blocks `UPDATE_BASELINES`. |
22
+ | `DEVICE_TEST_BASELINES` | absolute path | `<cwd>/baselines` | Override for the baselines root. Primary mechanism is `installDeviceTestHooks({ baselineRoot })`; this is the escape hatch for ad-hoc runs. |
23
+ | `DEVICE_TEST_RESOLUTION` | `hd` | `fhd` | — | Ad-hoc baseline resolution override. **Ignored when a client is registered** via `installDeviceTestHooks({ client })`. |
24
+ | `DEVICE_TEST_KEY_DELAY` | decimal ms | `100` | Default inter-key delay for `keysequence`. Override per call via `KeySequenceOptions.keyDelay`. |
25
+
26
+ ### Device credentials
27
+
28
+ The deploy path (`RkClient.buildAndDeploy` →
29
+ `buildAndLaunchWithParams`) needs **both** an IP and a developer-mode
30
+ password. They are resolved in this order:
31
+
32
+ 1. **Env-var pair** — `RK_DEVICE_IP` + `RK_DEVICE_PASSWORD`. When both
33
+ are set, they are used as-is and `.roku/` is not read. An empty
34
+ value counts as unset. Skipped when the caller passes an explicit
35
+ `BuildAndDeployOptions.deviceSelector` (a selector only makes sense
36
+ against a multi-device store).
37
+ 2. **`RK_DEVICE_IP` alone** — the device is looked up in
38
+ `.roku/roku-dev-config.json` by that IP (not by the active pointer),
39
+ and its stored password is used. This is the normal local run:
40
+ `globalSetup` exports the IP it resolved, so a run against a device
41
+ registered with `rk device add` / `rk device update` arrives here
42
+ with only the IP set.
43
+ 3. **`.roku/roku-dev-config.json`** — the active device record (or
44
+ the device matching `deviceSelector`), written by `rk device add`
45
+ / `rk device update`.
46
+
47
+ The env-var pair is the recommended path for CI, where credentials
48
+ typically live in masked pipeline variables and materialising a
49
+ `.roku/` config file would either leak the secret to disk or require
50
+ an extra scaffolding step. The file path is the recommended local
51
+ workflow because it's discoverable, persistent across shells, and
52
+ shared with the rest of the `rk` CLI.
53
+
54
+ Failure modes (all surface as `DeployFailedError({ phase: "config" })`
55
+ and mark the device unhealthy):
56
+
57
+ - Only `RK_DEVICE_PASSWORD` set → `"RK_DEVICE_PASSWORD is set but
58
+ RK_DEVICE_IP is empty; set both env vars or neither"`.
59
+ - Only `RK_DEVICE_IP` set, and no stored device has that IP →
60
+ `"RK_DEVICE_IP (X) is not a registered rk device; run `rk device
61
+ add X` and `rk device update X --password <pw>`, or set
62
+ RK_DEVICE_PASSWORD"`.
63
+ - `RK_DEVICE_IP` disagrees with the IP the `RkClient` was constructed
64
+ with (typically by `globalSetup`) → `"RK_DEVICE_IP env var (X)
65
+ does not match RkClient deviceIp (Y); fix one of them"`.
66
+ - Neither path produces a device → `"no active device configured;
67
+ run `rk device add <ip>` or set RK_DEVICE_IP +
68
+ RK_DEVICE_PASSWORD"`.
69
+ - `.roku/` device record has no password →
70
+ `"device '…' has no password; run `rk device update` to set one or
71
+ set RK_DEVICE_PASSWORD"`.
72
+
73
+ **Example — running against a specific device on a different `rk server`.**
74
+
75
+ ```bash
76
+ RK_DEVICE_IP=192.168.1.42 \
77
+ RK_SERVER_URL=http://10.0.0.5:3737 \
78
+ pnpm jest
79
+ ```
80
+
81
+ **Example — full CI invocation (no `.roku/` on disk).**
82
+
83
+ ```bash
84
+ RK_DEVICE_IP=192.168.1.42 \
85
+ RK_DEVICE_PASSWORD=$ROKU_DEV_PASSWORD_FROM_VAULT \
86
+ pnpm jest
87
+ ```
88
+
89
+ **Example — regenerating baselines locally.**
90
+
91
+ ```bash
92
+ UPDATE_BASELINES=1 pnpm jest tests/smoke.test.ts
93
+ ```
94
+
95
+ **Example — ad-hoc baseline inspection without a device.**
96
+
97
+ ```bash
98
+ DEVICE_TEST_RESOLUTION=fhd DEVICE_TEST_BASELINES=./baselines pnpm jest
99
+ ```
100
+
101
+ ## Tunable constants
102
+
103
+ Values exported as named constants from `@roku-sdk/device-test-client`.
104
+ Most can be overridden per-client via `RkClientOptions.intervals` or
105
+ per-call via option objects.
106
+
107
+ ### ECP timing
108
+
109
+ ```ts
110
+ const DEFAULT_KEY_DELAY = 100; // ms
111
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5000; // ms
112
+ const ECP_RETRY_BACKOFF_MS = 500; // ms (single retry)
113
+
114
+ const ECP_INTERVAL_MS: Record<EcpCategory, number> = {
115
+ "query-appui": 200,
116
+ "query-device-info": 0,
117
+ "query-active-app": 0,
118
+ keypress: 0,
119
+ launch: 0,
120
+ capture: 500
121
+ };
122
+ ```
123
+
124
+ `ECP_INTERVAL_MS` values can be per-client-overridden:
125
+
126
+ ```ts
127
+ const client = new RkClient({
128
+ deviceIp: "192.168.1.42",
129
+ intervals: {
130
+ "query-appui": 50, // DANGEROUS — here be dragons
131
+ capture: 200
132
+ }
133
+ });
134
+ ```
135
+
136
+ The intervals are **gap-based** (end of previous call to start of
137
+ next), not start-to-start, so tightening them beyond what the device
138
+ can sustain causes silent slow responses that look like timeouts.
139
+
140
+ ### Readiness
141
+
142
+ ```ts
143
+ const READINESS_TIMEOUT_MS = 10_000; // default for buildAndDeploy / launch
144
+ const READINESS_POLL_INTERVAL_MS = 200;
145
+ const READINESS_CONFIRM_POLLS = 2; // consecutive predicate passes
146
+ ```
147
+
148
+ Override per-call via `BuildAndDeployOptions.readinessTimeout` /
149
+ `LaunchOptions.readinessTimeout`.
150
+
151
+ ### Polling
152
+
153
+ ```ts
154
+ const WAIT_FOR_NODE_DEFAULT_INTERVAL_MS = 200;
155
+ const WAIT_FOR_LOG_DEFAULT_TIMEOUT_MS = 5_000;
156
+ const SCREENSHOT_STABLE_DEFAULT_INTERVAL_MS = 500;
157
+ ```
158
+
159
+ ### Screenshot comparison
160
+
161
+ ```ts
162
+ const DEFAULT_SCREENSHOT_THRESHOLD = 0.01; // 1% of unmasked pixels
163
+ const PIXELMATCH_COLOR_THRESHOLD = 0.1; // internal, NOT caller-tunable
164
+ const SCREENSHOT_DIFF_DIR = "test-results/diffs";
165
+ ```
166
+
167
+ ### Log buffer
168
+
169
+ ```ts
170
+ const LOG_BUFFER_MAX_LINES = 10_000;
171
+ const LOG_BUFFER_DROP_HALF = 5_000;
172
+ const SSE_PENDING_MAX_BYTES = 1_048_576; // SSE parser hard cap
173
+ ```
174
+
175
+ The underlying log buffer accepts `max` / `dropHalf` overrides for
176
+ unit tests via its deep-import module — see
177
+ [log-assertions.md](log-assertions.md#internal--deep-import-only).
178
+
179
+ ### System dialogs
180
+
181
+ ```ts
182
+ const SYSTEM_DIALOG_TAGS: ReadonlySet<string>;
183
+ // "Dialog" | "StandardMessageDialog" | "StandardDialog"
184
+ // | "ProblemDialog" | "KeyboardDialog" | "PinEntryDialog"
185
+ ```
186
+
187
+ ### Baselines
188
+
189
+ ```ts
190
+ const SUPPORTED_VIDEO_MODES: ReadonlySet<string>;
191
+ // "720p" | "720p60" | "1080p" | "1080p60"
192
+
193
+ const BASELINE_RESOLUTION_DIR = { hd: "hd", fhd: "fhd" };
194
+ ```
195
+
196
+ ### Failure bundles
197
+
198
+ ```ts
199
+ const FAILURES_DIR = "failures";
200
+ const FAILURE_ARTIFACT_FILE = {
201
+ SCREENSHOT: "screenshot.png",
202
+ DOM_TREE: "dom-tree.json",
203
+ RECENT_LOGS: "recent-logs.txt",
204
+ ERROR: "error.txt"
205
+ } as const;
206
+ const FAILURE_RECENT_LOG_LINES = 100;
207
+ ```
208
+
209
+ ### Diagnostics text
210
+
211
+ ```ts
212
+ const TREE_SUMMARY_MAX_DEPTH = 3;
213
+ const TREE_SUMMARY_MAX_CHARS = 2_000;
214
+ const TREE_DIFF_SNIPPET_MAX_CHARS = 200;
215
+ const WAIT_FOR_LOG_RECENT_LINES_MAX = 20;
216
+ const WAIT_FOR_LOG_RECENT_CHARS_MAX = 600;
217
+ ```
218
+
219
+ ### Handoff + daemon
220
+
221
+ ```ts
222
+ const ROKU_LOG_PORT = 8085;
223
+ const DEFAULT_RK_SERVER_URL = "http://localhost:3737/api";
224
+ const DEVICE_IP_CACHE_FILE = ".device-ip";
225
+
226
+ const ARTIFACT_FILE = {
227
+ DEVICE_ENV: "device-env.json",
228
+ JUNIT: "junit.xml",
229
+ DEVICE_LOG: "device.log"
230
+ } as const;
231
+ ```
232
+
233
+ ## `test-results/` layout
234
+
235
+ ```
236
+ test-results/
237
+ ├── .device-ip # resolved IP, written by globalSetup
238
+ ├── device-env.json # DeviceInfo snapshot + lease provenance
239
+ ├── device.log # every buffered log line, with header
240
+ ├── junit.xml # if a junit reporter is wired
241
+ ├── diffs/
242
+ │ └── home--initial.diff.png # screenshot mismatch artifacts
243
+ └── failures/
244
+ └── grid--grid-scroll--renders-12-items/
245
+ ├── screenshot.png
246
+ ├── dom-tree.json
247
+ ├── recent-logs.txt
248
+ └── error.txt
249
+ ```
250
+
251
+ All files are **rewritten on every run** — `cleanArtifacts` inside
252
+ `createGlobalSetup` wipes the directory (preserving only
253
+ `.gitkeep`) before the suite starts. Commit a `.gitkeep` if you want
254
+ the directory to exist in the repo.
255
+
256
+ Artifact filenames are stable constants (`ARTIFACT_FILE` and
257
+ `FAILURE_ARTIFACT_FILE`); CI reporters that upload them can rely on
258
+ the shape.