@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/src/constants.ts
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { EcpCategory } from "./types";
|
|
4
|
+
|
|
5
|
+
export const ENV_VAR = {
|
|
6
|
+
/** Set to `"true"` or `"1"` by CI runners. */
|
|
7
|
+
CI: "CI",
|
|
8
|
+
/** Set to `"1"` to overwrite committed baselines on the next run. */
|
|
9
|
+
UPDATE_BASELINES: "UPDATE_BASELINES",
|
|
10
|
+
/** Override the default inter-key delay (ms) for `client.keysequence`. */
|
|
11
|
+
KEY_DELAY: "DEVICE_TEST_KEY_DELAY",
|
|
12
|
+
/**
|
|
13
|
+
* Explicit override for the target Roku device IP. When set, bypasses
|
|
14
|
+
* `.roku/` active-device resolution. Must be a local-network IPv4.
|
|
15
|
+
*/
|
|
16
|
+
RK_DEVICE_IP: "RK_DEVICE_IP",
|
|
17
|
+
/**
|
|
18
|
+
* Roku developer-mode password. Set together with `RK_DEVICE_IP`
|
|
19
|
+
* to bypass `.roku/roku-dev-config.json`. A partial pair is a
|
|
20
|
+
* misconfiguration and fails loudly with `DeployFailedError`
|
|
21
|
+
* (silent fallback would mask the partial-config case).
|
|
22
|
+
*/
|
|
23
|
+
RK_DEVICE_PASSWORD: "RK_DEVICE_PASSWORD",
|
|
24
|
+
/**
|
|
25
|
+
* Absolute path override for the baselines root directory. Primary
|
|
26
|
+
* mechanism is `setBaselineRoot()`; this env var is the one-off
|
|
27
|
+
* escape hatch for scripts run outside the configured Jest setup.
|
|
28
|
+
*/
|
|
29
|
+
DEVICE_TEST_BASELINES: "DEVICE_TEST_BASELINES",
|
|
30
|
+
/**
|
|
31
|
+
* Resolution override (`"hd"` | `"fhd"`) for baseline-directory
|
|
32
|
+
* selection when no active `RkClient` has a cached `DeviceInfo`
|
|
33
|
+
* (e.g. ad-hoc CLI use). Ignored once a client has been registered
|
|
34
|
+
* via `setActiveClient()`.
|
|
35
|
+
*/
|
|
36
|
+
DEVICE_TEST_RESOLUTION: "DEVICE_TEST_RESOLUTION",
|
|
37
|
+
/**
|
|
38
|
+
* Base URL of the `rk server` daemon. Canonical shape is
|
|
39
|
+
* `http://host:port/api`; bare `http://host:port` is also accepted
|
|
40
|
+
* (normalizer appends `/api`). Defaults to `DEFAULT_RK_SERVER_URL`.
|
|
41
|
+
*/
|
|
42
|
+
RK_SERVER_URL: "RK_SERVER_URL",
|
|
43
|
+
/**
|
|
44
|
+
* Per-call idle timeout (ms) for the `POST /api/build` SSE stream.
|
|
45
|
+
* Caps sidecar wedges at a recoverable per-call failure instead
|
|
46
|
+
* of eating the whole CI job. Override when cold esbuild init on
|
|
47
|
+
* constrained CI runners exceeds the default
|
|
48
|
+
* `BUILD_SSE_IDLE_TIMEOUT_DEFAULT_MS`. Invalid values warn and
|
|
49
|
+
* fall back to the default — startup must never crash on a typo.
|
|
50
|
+
*/
|
|
51
|
+
BUILD_SSE_IDLE_TIMEOUT_MS: "RK_BUILD_SSE_IDLE_TIMEOUT_MS",
|
|
52
|
+
/**
|
|
53
|
+
* Identifier for the GitLab CI shard / job slice running this
|
|
54
|
+
* suite (e.g. "runtime", "components"). Surfaced in JUnit
|
|
55
|
+
* `<properties>` and the `[device-lease]` log so post-hoc
|
|
56
|
+
* telemetry can correlate per-shard wall time and device usage.
|
|
57
|
+
*
|
|
58
|
+
* Defaults to `"default"` when unset so JUnit consumers and
|
|
59
|
+
* dashboards never render an empty value (an empty value is
|
|
60
|
+
* indistinguishable from a missing property in most viewers).
|
|
61
|
+
*
|
|
62
|
+
* Forms part of the parallel-device telemetry baseline and is
|
|
63
|
+
* consumed verbatim by the parallel-shard JUnit naming. Defining
|
|
64
|
+
* it here lets the env-allowlist reference the same name without
|
|
65
|
+
* churn.
|
|
66
|
+
*/
|
|
67
|
+
RK_SHARD_NAME: "RK_SHARD_NAME",
|
|
68
|
+
/**
|
|
69
|
+
* Identifier for the device-pool provider that supplied the
|
|
70
|
+
* device for this run (e.g. `"local"`, `"cloud"`).
|
|
71
|
+
* Surfaced in JUnit `<properties>` and the `[device-lease]`
|
|
72
|
+
* log so telemetry can attribute regressions to a specific
|
|
73
|
+
* provider tier.
|
|
74
|
+
*
|
|
75
|
+
* **Forward-compat note.** The original spec called for a
|
|
76
|
+
* hardcoded `"local"` literal at this stage. We plumb an env
|
|
77
|
+
* var with a `"local"` default instead so:
|
|
78
|
+
* - Baseline runs emit the same value (no behavioral
|
|
79
|
+
* diff against the spec).
|
|
80
|
+
* - Provider-backed pools can set this
|
|
81
|
+
* env var from their CLI without retrofitting
|
|
82
|
+
* `device-test-client`.
|
|
83
|
+
*
|
|
84
|
+
* If the provider seam ends up choosing a different injection
|
|
85
|
+
* mechanism (e.g. `ResolvedConfig.pool` via a runtime setter),
|
|
86
|
+
* this env var becomes dead and should be removed.
|
|
87
|
+
*/
|
|
88
|
+
RK_PROVIDER_KIND: "RK_PROVIDER_KIND",
|
|
89
|
+
/**
|
|
90
|
+
* ULID of the device-pool lease the current test process should
|
|
91
|
+
* read its target IP + passwordRef from. When set, the resolver's
|
|
92
|
+
* **Layer 0** reads `.roku/leases/<id>.json`, Zod-validates it
|
|
93
|
+
* through `DeviceLeaseSchema`, and returns the embedded `ip` /
|
|
94
|
+
* resolved `passwordRef`.
|
|
95
|
+
*
|
|
96
|
+
* Precedence inside the resolver (later overrides earlier):
|
|
97
|
+
* 1. (Layer 0) `RK_DEVICE_LEASE_ID` — set by `rk device lease
|
|
98
|
+
* acquire`; the canonical CI path for provider-backed pools.
|
|
99
|
+
* 2. `RK_DEVICE_IP` — partner-side explicit override; STILL
|
|
100
|
+
* wins on conflict so we don't surprise partner pipelines.
|
|
101
|
+
* 3. `.roku/` active device → first registered device (today's
|
|
102
|
+
* behavior).
|
|
103
|
+
*
|
|
104
|
+
* Forms the producer side of the parallel-CI model. The acquiring
|
|
105
|
+
* provider writes the lease file; this env var pins
|
|
106
|
+
* the consumer at boot to the correct lease without reading
|
|
107
|
+
* any workspace-singleton state (`.roku/roku-dev-config.json`
|
|
108
|
+
* cannot be safely shared across parallel jobs).
|
|
109
|
+
*/
|
|
110
|
+
RK_DEVICE_LEASE_ID: "RK_DEVICE_LEASE_ID",
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Directory holding lease files, defaulting to `<cwd>/.roku/leases`.
|
|
114
|
+
* Set by CI (and `scripts/run-shards-on-devices.sh`) so a job running
|
|
115
|
+
* from a subdirectory still finds the lease the acquire step wrote.
|
|
116
|
+
*
|
|
117
|
+
* Every reader must resolve it identically — two readers disagreeing on
|
|
118
|
+
* the directory silently read two different leases.
|
|
119
|
+
*/
|
|
120
|
+
RK_DEVICE_LEASE_DIR: "RK_DEVICE_LEASE_DIR"
|
|
121
|
+
} as const;
|
|
122
|
+
|
|
123
|
+
/** Default delay between successive ECP keypresses, in milliseconds. */
|
|
124
|
+
export const DEFAULT_KEY_DELAY = 100;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Default per-request ECP timeout for non-capture calls. 10s (not 5s)
|
|
128
|
+
* because `/launch/dev` against an Instant-Resume manifest can take
|
|
129
|
+
* several seconds on slow hardware before returning 200.
|
|
130
|
+
*/
|
|
131
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Retry backoff after a transient ECP failure (5xx or timeout). Callers
|
|
135
|
+
* see a single retry; if the second attempt fails the original error
|
|
136
|
+
* propagates.
|
|
137
|
+
*/
|
|
138
|
+
export const ECP_RETRY_BACKOFF_MS = 500;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Per-category minimum gap (ms) between the END of the previous call
|
|
142
|
+
* and the START of the next. `app-ui` 200ms (waitForNode cadence),
|
|
143
|
+
* `capture` 500ms (expensive); other categories 0 (still serialized).
|
|
144
|
+
*/
|
|
145
|
+
export const ECP_INTERVAL_MS: Readonly<Record<EcpCategory, number>> = {
|
|
146
|
+
"query-appui": 200,
|
|
147
|
+
"query-device-info": 0,
|
|
148
|
+
"query-active-app": 0,
|
|
149
|
+
keypress: 0,
|
|
150
|
+
launch: 0,
|
|
151
|
+
capture: 500
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Video modes the framework recognises. Anything else fails fast at startup. */
|
|
155
|
+
export const SUPPORTED_VIDEO_MODES: ReadonlySet<string> = new Set(["720p", "720p60", "1080p", "1080p60"]);
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Device recovery (crash / reboot resilience)
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Default budget for `waitForDeviceOnline`. Roku crash-recovery
|
|
163
|
+
* (reboot + home-screen mount + ECP responder) lands in 60–120s on
|
|
164
|
+
* reference hardware; 180s covers cold-boot on Roku Express without
|
|
165
|
+
* letting a permanent outage hang silently.
|
|
166
|
+
*/
|
|
167
|
+
export const DEVICE_ONLINE_DEFAULT_TIMEOUT_MS = 180_000;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Probe interval inside `waitForDeviceOnline`. Sub-second cadence has
|
|
171
|
+
* been observed to starve `/logs/*` transitions on the rk-server side
|
|
172
|
+
* (single-threaded ECP responder during the crash window).
|
|
173
|
+
*/
|
|
174
|
+
export const DEVICE_ONLINE_POLL_INTERVAL_MS = 2_000;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Default budget for the installer-port (HTTP/80) phase of
|
|
178
|
+
* `waitForDeviceFullyReady`. The installer is what `rk` POSTs the
|
|
179
|
+
* sideload package to, and on reference hardware it lags ECP recovery
|
|
180
|
+
* by a few seconds after a crash/reboot — long enough that probing only
|
|
181
|
+
* `/query/device-info` (port 8060) and immediately retrying a deploy
|
|
182
|
+
* yields the misleading "Device is not reachable on the network"
|
|
183
|
+
* diagnostic. 60s covers the observed lag with headroom while still
|
|
184
|
+
* keeping the overall guard budget bounded.
|
|
185
|
+
*/
|
|
186
|
+
export const INSTALLER_PORT_PROBE_DEFAULT_TIMEOUT_MS = 60_000;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Probe interval for the installer-port arm of
|
|
190
|
+
* `waitForDeviceFullyReady`. 1s is fast enough to catch the typical
|
|
191
|
+
* 2–10s post-ECP installer recovery window without flooding the device
|
|
192
|
+
* with TCP SYNs while it is still booting.
|
|
193
|
+
*/
|
|
194
|
+
export const INSTALLER_PORT_PROBE_INTERVAL_MS = 1_000;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Per-attempt TCP-connect timeout for the installer-port probe. Mirrors
|
|
198
|
+
* `tcpProbe`'s default in `@roku-sdk/tooling`; surfaced here so tests
|
|
199
|
+
* and callers can reason about probe latency without crossing package
|
|
200
|
+
* boundaries.
|
|
201
|
+
*/
|
|
202
|
+
export const INSTALLER_PORT_TCP_TIMEOUT_MS = 1_500;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Test-level retries for device recovery. Higher counts mask real
|
|
206
|
+
* regressions; lower counts leave first-observed crashes unrecovered.
|
|
207
|
+
*/
|
|
208
|
+
export const DEVICE_RECOVERY_DEFAULT_RETRIES = 3;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Default post-sideload readiness deadline. Covers cold-start on the
|
|
212
|
+
* slowest reference hardware (Express ~3s, Ultra ~1s) with headroom
|
|
213
|
+
* for GC/HDCP stalls. Pass `readinessTimeout` explicitly for slower fixtures.
|
|
214
|
+
*/
|
|
215
|
+
export const READINESS_TIMEOUT_MS = 10_000;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Sleep between readiness probes on the `/query/active-app` arm
|
|
219
|
+
* (which has a 0ms queue gap). Avoids DoS-ing the device during
|
|
220
|
+
* sideload transitions.
|
|
221
|
+
*/
|
|
222
|
+
export const READINESS_POLL_INTERVAL_MS = 200;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Consecutive predicate passes before declaring ready. Two catches
|
|
226
|
+
* crashes shortly after first render (observed `EXIT_APP_ERROR` ~300ms
|
|
227
|
+
* after splash); higher counts add deploy latency for diminishing
|
|
228
|
+
* returns since later `assertAppAlive` calls cover longer windows.
|
|
229
|
+
*/
|
|
230
|
+
export const READINESS_CONFIRM_POLLS = 2;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Fraction of the readiness budget tolerating post-launch home-screen
|
|
234
|
+
* responses before a previously-observed app is treated as
|
|
235
|
+
* `crashed-after-ready`. `/launch/dev` returns 200 before firmware
|
|
236
|
+
* swaps the active app (~200–500ms transition), and InstantOn can
|
|
237
|
+
* briefly bounce through Home after first render. A never-observed app
|
|
238
|
+
* is classified only at the full readiness timeout; slow cold sideloads
|
|
239
|
+
* can remain on Home longer than this grace window and still launch.
|
|
240
|
+
*/
|
|
241
|
+
export const HOME_SCREEN_GRACE_FRACTION = 0.25;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Absolute floor for the home-screen grace window. Without it, a
|
|
245
|
+
* sub-second readiness budget would compute a grace below the
|
|
246
|
+
* `/launch/dev` → off-home transition and false-positive.
|
|
247
|
+
*/
|
|
248
|
+
export const HOME_SCREEN_GRACE_MIN_MS = 500;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Tag names of root-level system dialogs (e.g. "Display type detection").
|
|
252
|
+
* Matching on tag at the root rules out app-authored `Dialog` subclasses
|
|
253
|
+
* (those live inside a mounted Scene, not as root siblings).
|
|
254
|
+
*/
|
|
255
|
+
export const SYSTEM_DIALOG_TAGS: ReadonlySet<string> = new Set([
|
|
256
|
+
"Dialog",
|
|
257
|
+
"StandardMessageDialog",
|
|
258
|
+
"StandardDialog",
|
|
259
|
+
"ProblemDialog",
|
|
260
|
+
"KeyboardDialog",
|
|
261
|
+
"PinEntryDialog"
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Default `waitForNode` cadence; matches the `/query/app-ui` queue gap
|
|
266
|
+
* so polling lands naturally on the queue's rhythm.
|
|
267
|
+
*/
|
|
268
|
+
export const WAIT_FOR_NODE_DEFAULT_INTERVAL_MS = 200;
|
|
269
|
+
|
|
270
|
+
/** Max serialized depth in `summarizeTree`; deeper subtrees become `[N children]`. */
|
|
271
|
+
export const TREE_SUMMARY_MAX_DEPTH = 3;
|
|
272
|
+
|
|
273
|
+
/** Hard cap on `summarizeTree` output length. */
|
|
274
|
+
export const TREE_SUMMARY_MAX_CHARS = 2000;
|
|
275
|
+
|
|
276
|
+
/** Snippet length for expected/actual JSON values in `TreeMismatchError`. */
|
|
277
|
+
export const TREE_DIFF_SNIPPET_MAX_CHARS = 200;
|
|
278
|
+
|
|
279
|
+
/** Baseline directory name per-resolution. */
|
|
280
|
+
export const BASELINE_RESOLUTION_DIR = {
|
|
281
|
+
hd: "hd",
|
|
282
|
+
fhd: "fhd"
|
|
283
|
+
} as const;
|
|
284
|
+
|
|
285
|
+
/** Filenames written under each consumer's `test-results/` directory. */
|
|
286
|
+
export const ARTIFACT_FILE = {
|
|
287
|
+
DEVICE_ENV: "device-env.json",
|
|
288
|
+
JUNIT: "junit.xml",
|
|
289
|
+
DEVICE_LOG: "device.log",
|
|
290
|
+
/**
|
|
291
|
+
* CommonJS file written by the telemetry helper that the
|
|
292
|
+
* `jest-junit` reporter `require()`s at JUnit generation
|
|
293
|
+
* time to add per-`<testcase>` `<properties>` entries
|
|
294
|
+
* (`device-id`, `shard-name`, `providerKind`). Picked up via
|
|
295
|
+
* `jest-junit`'s `testCasePropertiesFile` /
|
|
296
|
+
* `testCasePropertiesDirectory` options. Filename is exposed
|
|
297
|
+
* as a constant so the consumer's `jest.config.ts` and the
|
|
298
|
+
* writer in `telemetry.ts` reference the same string.
|
|
299
|
+
*/
|
|
300
|
+
JUNIT_CASE_PROPERTIES: "junit-test-case-properties.js"
|
|
301
|
+
} as const;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Optional `.passthrough()` riders on `.roku/leases/<id>.json` that this
|
|
305
|
+
* package reads. Not declared by `DeviceLeaseSchema`, so the names are the
|
|
306
|
+
* whole contract — they must match the provider that stamps them (see the
|
|
307
|
+
* "Known passthrough riders" list in
|
|
308
|
+
* `@roku-sdk/tooling/src/device-pool/schemas/lease.ts`).
|
|
309
|
+
*/
|
|
310
|
+
export const LEASE_FIELD = {
|
|
311
|
+
/**
|
|
312
|
+
* OS image the pool provider flashed the device with before handing out
|
|
313
|
+
* the lease. Recorded verbatim in `device-env.json` so a failed run can
|
|
314
|
+
* be traced back to an image: the ECP `firmwareVersion` is too coarse
|
|
315
|
+
* (several images report the same `ui-software` version) and the
|
|
316
|
+
* provider-side libraries (`grandcentral`, `trc-channel`) can differ
|
|
317
|
+
* between two devices that report identical firmware.
|
|
318
|
+
*/
|
|
319
|
+
ROOTFS_ARTIFACTORY_PATH: "rootfsArtifactoryPath"
|
|
320
|
+
} as const;
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Failure diagnostics
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/** Directory (relative to `testResultsDir`) for per-failure bundles. */
|
|
327
|
+
export const FAILURES_DIR = "failures";
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Filenames inside each per-failure bundle. Keep in sync with
|
|
331
|
+
* `writeFailureBundle` — CI artifact pipelines key off these names.
|
|
332
|
+
*/
|
|
333
|
+
export const FAILURE_ARTIFACT_FILE = {
|
|
334
|
+
SCREENSHOT: "screenshot.png",
|
|
335
|
+
DOM_TREE: "dom-tree.json",
|
|
336
|
+
RECENT_LOGS: "recent-logs.txt",
|
|
337
|
+
ERROR: "error.txt",
|
|
338
|
+
/**
|
|
339
|
+
* `LogStreamHealth` snapshot at failure time; lets a reviewer
|
|
340
|
+
* attribute a log timeout to either the device→server telnet leg
|
|
341
|
+
* or the server→client SSE leg without rerunning.
|
|
342
|
+
*/
|
|
343
|
+
STREAM_HEALTH: "stream-health.json"
|
|
344
|
+
} as const;
|
|
345
|
+
|
|
346
|
+
/** Default tail size for `recent-logs.txt`. */
|
|
347
|
+
export const FAILURE_RECENT_LOG_LINES = 100;
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Global keys shared between consumer `setup.ts` and the custom Jest
|
|
351
|
+
* test environment. Globals (not env options) because Jest constructs
|
|
352
|
+
* the environment in a different module context from `setup.ts` and
|
|
353
|
+
* globals are the only surface both sides reach.
|
|
354
|
+
*/
|
|
355
|
+
export const DEVICE_ENV_GLOBAL_KEY = {
|
|
356
|
+
CLIENT: "__DEVICE_CLIENT__",
|
|
357
|
+
TEST_RESULTS_DIR: "__DEVICE_TEST_RESULTS_DIR__",
|
|
358
|
+
LOG_SINK: "__DEVICE_LOG_SINK__",
|
|
359
|
+
/**
|
|
360
|
+
* Opt-out switch for the failure-bundle pipeline. Written
|
|
361
|
+
* synchronously at install time (not in `beforeAll`) so the first
|
|
362
|
+
* `test_fn_failure` event already sees the final value. Absent
|
|
363
|
+
* key + absent `TEST_RESULTS_DIR` → env stays silent (vanilla
|
|
364
|
+
* `jest-environment-node` consumers don't get unsolicited bundles).
|
|
365
|
+
*/
|
|
366
|
+
CAPTURE_ENABLED: "__DEVICE_CAPTURE_ENABLED__"
|
|
367
|
+
} as const;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Per-suite cache of the IP resolved by `globalSetup`. Workers read
|
|
371
|
+
* synchronously from `createDefaultClient()` since `listMergedDevices`
|
|
372
|
+
* is async and the worker-side factory is sync.
|
|
373
|
+
*/
|
|
374
|
+
export const DEVICE_IP_CACHE_FILE = ".device-ip";
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Log assertions
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Default `rk server` base URL. Canonical form includes `/api` (matches
|
|
382
|
+
* `apiEndpoint` in `.roku/rk-server.json`); bare `http://host:port` is
|
|
383
|
+
* normalized by `resolveServerUrl`.
|
|
384
|
+
*/
|
|
385
|
+
export const DEFAULT_RK_SERVER_URL = "http://localhost:3737/api";
|
|
386
|
+
|
|
387
|
+
/** Roku device console log port. Mirror of `tooling-utils/constants.ts → ROKU_LOG_PORT`. */
|
|
388
|
+
export const ROKU_LOG_PORT = 8085;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Hard cap on retained log entries per worker. ~10k × ~200 B ≈ 2MB
|
|
392
|
+
* keeps a long-running suite within memory bounds; an earlier 200k
|
|
393
|
+
* cap drove the device-test container past its cgroup limit.
|
|
394
|
+
*/
|
|
395
|
+
export const LOG_BUFFER_MAX_LINES = 10_000;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Oldest entries dropped on cap. Half-drop is amortised O(1) per
|
|
399
|
+
* append vs drop-one-per-append; the doubled retention window absorbs
|
|
400
|
+
* short `since`-pos snapshots without surprise overflows.
|
|
401
|
+
*/
|
|
402
|
+
export const LOG_BUFFER_DROP_HALF = 5_000;
|
|
403
|
+
|
|
404
|
+
/** Default `waitForLog` timeout. */
|
|
405
|
+
export const WAIT_FOR_LOG_DEFAULT_TIMEOUT_MS = 5_000;
|
|
406
|
+
|
|
407
|
+
/** Upper bound on `recentLines` rendered into `WaitForLogTimeoutError`. */
|
|
408
|
+
export const WAIT_FOR_LOG_RECENT_LINES_MAX = 20;
|
|
409
|
+
|
|
410
|
+
/** Character cap on the recentLines preview block. */
|
|
411
|
+
export const WAIT_FOR_LOG_RECENT_CHARS_MAX = 600;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* SSE parser `pending` accumulator cap. Frames are normally a few
|
|
415
|
+
* hundred bytes; a stream past 1 MiB without `\n\n` is broken or
|
|
416
|
+
* hostile — fail loud rather than leak memory.
|
|
417
|
+
*/
|
|
418
|
+
export const SSE_PENDING_MAX_BYTES = 1_048_576;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Default per-call idle timeout (ms) for the `POST /api/build` SSE
|
|
422
|
+
* stream consumed by `requestServerBuild`. Sized for the worst-case
|
|
423
|
+
* cold path on constrained CI runners: first-time esbuild service
|
|
424
|
+
* spawn (~10–15s on shared CI containers) + Hermes warmup
|
|
425
|
+
* (~30–60s) + first incremental TS resolve (~30s). 90s — the prior
|
|
426
|
+
* value — could trip on cold starts and surface as the opaque
|
|
427
|
+
* `RK_SERVER_BUILD_IDLE_TIMEOUT` code, indistinguishable from a
|
|
428
|
+
* real build error. 180s comfortably covers cold-start without
|
|
429
|
+
* letting a permanent sidecar wedge eat the whole job's budget.
|
|
430
|
+
*
|
|
431
|
+
* Override per-environment via the `RK_BUILD_SSE_IDLE_TIMEOUT_MS`
|
|
432
|
+
* env var (any positive integer, in ms). Invalid values warn and
|
|
433
|
+
* fall back to this default rather than crashing the test run.
|
|
434
|
+
*/
|
|
435
|
+
export const BUILD_SSE_IDLE_TIMEOUT_DEFAULT_MS = 180_000;
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// Screenshot assertions
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Default convergence interval for `captureStableScreenshot`. Matches
|
|
443
|
+
* the ECP queue's `capture` gap so polling lands on the queue's cadence.
|
|
444
|
+
*/
|
|
445
|
+
export const SCREENSHOT_STABLE_DEFAULT_INTERVAL_MS = 500;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Default pixel-mismatch ratio for `expectScreenshot` (1% of the
|
|
449
|
+
* unmasked canvas). Tolerates anti-aliasing drift without missing
|
|
450
|
+
* legitimate content changes.
|
|
451
|
+
*/
|
|
452
|
+
export const DEFAULT_SCREENSHOT_THRESHOLD = 0.01;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Per-pixel YIQ cutoff for `pixelmatch`. Decoupled from the caller-facing
|
|
456
|
+
* threshold (which is the aggregate-ratio knob); 0.1 is pixelmatch's
|
|
457
|
+
* own library default.
|
|
458
|
+
*/
|
|
459
|
+
export const PIXELMATCH_COLOR_THRESHOLD = 0.1;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Per-axis ceiling for `ScreenshotMatchOptions.alignTolerance`. The
|
|
463
|
+
* search is O((2x+1)(2y+1)) candidate offsets, so the cap bounds both
|
|
464
|
+
* the runtime and how far the comparator may slide a capture before
|
|
465
|
+
* scoring it — a wide window could "align" genuinely different content.
|
|
466
|
+
* Observed device-dependent OSD re-centering is 1-3px.
|
|
467
|
+
*/
|
|
468
|
+
export const ALIGN_TOLERANCE_MAX_PX = 4;
|
|
469
|
+
|
|
470
|
+
/** Default diff-PNG directory; overridable via `setScreenshotDiffRoot`. */
|
|
471
|
+
export const SCREENSHOT_DIFF_DIR = "test-results/diffs";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* `modelName` → coarse performance tier for precondition gating.
|
|
5
|
+
* Separate from `DeviceInfo.deviceClass` (which is a form-factor
|
|
6
|
+
* classifier). Hand-maintained because the model→tier mapping is
|
|
7
|
+
* not algorithmic. Unknown models → `"unknown"` so preconditions
|
|
8
|
+
* fail closed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type DeviceClass = "high-end" | "mid-range" | "low-end" | "unknown";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalise for table lookup: lowercase, trim, strip `"roku "`
|
|
15
|
+
* prefix so `"Roku Ultra"` and `"Ultra"` resolve identically.
|
|
16
|
+
*/
|
|
17
|
+
function normalizeModel(modelName: string): string {
|
|
18
|
+
const lower = modelName.trim().toLowerCase();
|
|
19
|
+
return lower.startsWith("roku ") ? lower.slice("roku ".length) : lower;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Static `modelName` → tier table. Conservative: prefer `"unknown"`
|
|
24
|
+
* for new models until they're manually classified.
|
|
25
|
+
*/
|
|
26
|
+
const MODEL_CLASS_TABLE: ReadonlyMap<string, DeviceClass> = new Map([
|
|
27
|
+
["express", "low-end"],
|
|
28
|
+
["express 4k", "mid-range"],
|
|
29
|
+
["express 4k+", "mid-range"],
|
|
30
|
+
["streaming stick", "low-end"],
|
|
31
|
+
["streaming stick+", "mid-range"],
|
|
32
|
+
["streaming stick 4k", "mid-range"],
|
|
33
|
+
["streaming stick 4k+", "mid-range"],
|
|
34
|
+
["premiere", "mid-range"],
|
|
35
|
+
["premiere+", "mid-range"],
|
|
36
|
+
["ultra", "high-end"],
|
|
37
|
+
["ultra lt", "mid-range"],
|
|
38
|
+
// Roku TVs keyed by the strings ECP actually reports.
|
|
39
|
+
["roku tv", "mid-range"],
|
|
40
|
+
["4k roku tv", "mid-range"],
|
|
41
|
+
["roku ultra lt", "mid-range"],
|
|
42
|
+
["reference rsg-4k", "high-end"],
|
|
43
|
+
["reference rsg-hd", "low-end"],
|
|
44
|
+
["streambar", "low-end"],
|
|
45
|
+
["streambar pro", "mid-range"],
|
|
46
|
+
["wireless soundbar", "mid-range"]
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map `modelName` to a tier. Pure (no I/O, no state).
|
|
51
|
+
* `"unknown"` for unmapped models so preconditions fail closed.
|
|
52
|
+
*/
|
|
53
|
+
export function getDeviceClass(modelName: string): DeviceClass {
|
|
54
|
+
if (!modelName) return "unknown";
|
|
55
|
+
const hit = MODEL_CLASS_TABLE.get(normalizeModel(modelName));
|
|
56
|
+
return hit ?? "unknown";
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Synchronous `DeviceInfo` accessor backed by
|
|
5
|
+
* `test-results/device-env.json` (written by `globalSetup`).
|
|
6
|
+
*
|
|
7
|
+
* `itOnDevice`/`describeOnDevice` evaluate preconditions at
|
|
8
|
+
* registration time — before any `beforeAll` has fired, so the
|
|
9
|
+
* live `RkClient` has no cached info yet. Reading from disk is the
|
|
10
|
+
* simplest shape that preserves the "one ECP round-trip per suite"
|
|
11
|
+
* contract.
|
|
12
|
+
*
|
|
13
|
+
* Missing file → return null (caller falls back to "register
|
|
14
|
+
* normally", no skip) so a fresh clone boots cleanly.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
import { ARTIFACT_FILE } from "../constants";
|
|
21
|
+
import { TEST_RESULTS_DIR_NAME } from "../paths";
|
|
22
|
+
import type { DeviceInfo } from "../types";
|
|
23
|
+
|
|
24
|
+
// Path resolved lazily from cwd so a `chdir` before hooks still
|
|
25
|
+
// lands correctly. Consumers override via `setDeviceEnvFile()` to
|
|
26
|
+
// anchor on `__dirname` (robust to worker cwd changes).
|
|
27
|
+
let overridePath: string | null = null;
|
|
28
|
+
|
|
29
|
+
export function setDeviceEnvFile(absPath: string): void {
|
|
30
|
+
overridePath = absPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getDeviceEnvFile(): string {
|
|
34
|
+
return overridePath ?? join(process.cwd(), TEST_RESULTS_DIR_NAME, ARTIFACT_FILE.DEVICE_ENV);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read and parse `device-env.json`. Returns null when missing or
|
|
39
|
+
* unparseable so precondition evaluators see "no cache" consistently.
|
|
40
|
+
* Synchronous because `itOnDevice`/`describeOnDevice` can't await.
|
|
41
|
+
*/
|
|
42
|
+
export function readCachedDeviceInfoSync(): DeviceInfo | null {
|
|
43
|
+
const file = getDeviceEnvFile();
|
|
44
|
+
if (!existsSync(file)) return null;
|
|
45
|
+
try {
|
|
46
|
+
const raw = readFileSync(file, "utf8");
|
|
47
|
+
const parsed = JSON.parse(raw) as DeviceInfo;
|
|
48
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
49
|
+
return parsed;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Test-only reset for the `setDeviceEnvFile` override. */
|
|
56
|
+
export function resetDeviceEnvFileOverride(): void {
|
|
57
|
+
overridePath = null;
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Device IP resolution — sync side (worker-safe).
|
|
5
|
+
*
|
|
6
|
+
* Intentionally has no `@roku-sdk/tooling` imports: the tooling's
|
|
7
|
+
* device-store module is ESM and breaks CommonJS consumers at load
|
|
8
|
+
* time via `import.meta.url`. Keep this file import-light.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { DEVICE_IP_CACHE_FILE, ENV_VAR } from "../constants";
|
|
15
|
+
import { RkClientError } from "../errors";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return the device IP to use from:
|
|
19
|
+
* 1. `RK_DEVICE_IP` env var, then
|
|
20
|
+
* 2. `testResultsDir/.device-ip` written by `globalSetup`.
|
|
21
|
+
*
|
|
22
|
+
* Throws `RkClientError(RK_DEVICE_IP_UNRESOLVED)` otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveDeviceIpSync(testResultsDir?: string): string {
|
|
25
|
+
const envIp = process.env[ENV_VAR.RK_DEVICE_IP];
|
|
26
|
+
if (envIp && envIp.length > 0) return envIp;
|
|
27
|
+
|
|
28
|
+
if (testResultsDir) {
|
|
29
|
+
try {
|
|
30
|
+
const cached = readFileSync(join(testResultsDir, DEVICE_IP_CACHE_FILE), "utf8").trim();
|
|
31
|
+
if (cached.length > 0) return cached;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
34
|
+
if (code !== "ENOENT") throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new RkClientError(
|
|
39
|
+
`Unable to resolve a Roku device IP. Set ${ENV_VAR.RK_DEVICE_IP}=<ip> or ensure ` +
|
|
40
|
+
`the suite's global-setup has run and written ${DEVICE_IP_CACHE_FILE}.`,
|
|
41
|
+
"RK_DEVICE_IP_UNRESOLVED"
|
|
42
|
+
);
|
|
43
|
+
}
|