@roku-sdk/device-test-client 0.0.0-stage → 2.18.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/LICENSE +21 -0
- package/README.md +144 -2
- package/docs/baselines.md +221 -0
- package/docs/configuration.md +258 -0
- package/docs/dom-assertions.md +264 -0
- package/docs/errors.md +525 -0
- package/docs/examples/deep-link-cold-start.md +85 -0
- package/docs/examples/focus-navigation.md +81 -0
- package/docs/examples/log-driven-assertion.md +99 -0
- package/docs/examples/precondition-skips.md +150 -0
- package/docs/examples/screenshot-regression.md +103 -0
- package/docs/examples/smoke-test.md +69 -0
- package/docs/getting-started.md +195 -0
- package/docs/jest-integration.md +331 -0
- package/docs/log-assertions.md +314 -0
- package/docs/preconditions.md +317 -0
- package/docs/rkclient.md +655 -0
- package/docs/screenshot-assertions.md +316 -0
- package/internal/constants.ts +54 -0
- package/internal/deploy.ts +60 -0
- package/internal/paths.ts +15 -0
- package/package.json +80 -3
- package/src/assertions/assertions.ts +661 -0
- package/src/assertions/baseline-env-guard.ts +43 -0
- package/src/assertions/baselines.ts +178 -0
- package/src/assertions/screenshot-artifacts.ts +39 -0
- package/src/assertions/screenshot-diff.ts +447 -0
- package/src/client/build-cache.ts +292 -0
- package/src/client/client.ts +1656 -0
- package/src/client/deploy.ts +402 -0
- package/src/client/ecp-queue.ts +112 -0
- package/src/client/ecp-retry.ts +94 -0
- package/src/client/factory.ts +147 -0
- package/src/client/health.ts +214 -0
- package/src/client/parsers.ts +155 -0
- package/src/client/server-url.ts +137 -0
- package/src/constants.ts +471 -0
- package/src/device/device-class.ts +57 -0
- package/src/device/device-env-sync.ts +58 -0
- package/src/device/device-resolver-sync.ts +43 -0
- package/src/device/device-resolver.ts +141 -0
- package/src/device/lease-provisioning.ts +54 -0
- package/src/errors.ts +773 -0
- package/src/index.ts +186 -0
- package/src/internal/constants.ts +3 -0
- package/src/jest/artifacts.ts +87 -0
- package/src/jest/device-test-environment.ts +420 -0
- package/src/jest/failure-artifacts.ts +169 -0
- package/src/jest/global-setup.ts +133 -0
- package/src/jest/global-teardown.ts +132 -0
- package/src/jest/install-hooks.ts +488 -0
- package/src/jest/jest-setup.ts +53 -0
- package/src/jest/preconditions.ts +239 -0
- package/src/jest/telemetry.ts +331 -0
- package/src/logs/device-log-sink.ts +69 -0
- package/src/logs/log-buffer.ts +265 -0
- package/src/logs/log-session.ts +374 -0
- package/src/logs/log-stream-client.ts +296 -0
- package/src/paths.ts +65 -0
- package/src/types.ts +443 -0
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
|
-
#
|
|
1
|
+
# @roku-sdk/device-test-client
|
|
2
2
|
|
|
3
|
-
|
|
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.
|