@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,137 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * `resolveServerUrl` — single source of truth for where the
5
+ * `rk server` daemon lives, shared between `RkClient` construction
6
+ * and `globalSetup`/`globalTeardown`.
7
+ *
8
+ * Why a dedicated module instead of an inline helper? Jest's
9
+ * `globalTeardown` runs in a separate module graph from the worker
10
+ * processes that construct `RkClient`, so we need a tree-shake-safe
11
+ * import path that does not pull in the full `client.ts` surface
12
+ * (with its `pngjs`, `@roku-sdk/tooling/*` deps) into the teardown
13
+ * lifecycle. Isolating this helper keeps teardown lean and makes the
14
+ * resolution order easy to audit in one place.
15
+ *
16
+ * Resolution order (first non-empty wins):
17
+ * 1. `opts.serverUrl` — explicit override.
18
+ * 2. `process.env[RK_SERVER_URL]` — per-process env var.
19
+ * 3. `.roku/rk-server.json#apiEndpoint` — discovery file the
20
+ * `rk server` CLI writes on startup. The port is derived from a
21
+ * sha256 of the workspace path (see
22
+ * `@roku-sdk/tooling` `server/port-derivation.ts`), so the
23
+ * default `:3737` is almost never the right answer for a real
24
+ * developer box. Reading this file lets the fixture's log pillar
25
+ * "just work" after the user starts `rk server` — no manual
26
+ * `RK_SERVER_URL=…` export required.
27
+ * 4. `DEFAULT_RK_SERVER_URL` — compiled-in fallback.
28
+ *
29
+ * Discovery is intentionally best-effort: a torn read during a
30
+ * server restart, a missing file, or a malformed JSON payload all
31
+ * fall through silently to the next resolution tier.
32
+ *
33
+ * Output normalization. Whatever tier wins, the returned URL is
34
+ * normalized to end with `/api` (no trailing slash). All consumers
35
+ * (`LogSession.postStart`, `LogStreamClient`, `globalSetup`'s
36
+ * `resetAndStartLogSession`, `RkClient.getLogStreamHealth`) build
37
+ * request URLs by appending `/logs/...` to this base, and every
38
+ * route on the server is mounted under `/api/...`
39
+ * (`tooling/src/server/index.ts:387: app.use("/api/logs", logRoutes)`).
40
+ * The discovery file already emits the canonical `http://host:port/api`
41
+ * shape (`coordination.ts::createPortFileData`); the env var path
42
+ * historically allowed a bare `http://host:port` and silently 404'd.
43
+ * Normalizing here makes both tiers indistinguishable to consumers
44
+ * and means a developer can set `RK_SERVER_URL` without remembering
45
+ * the `/api` suffix.
46
+ */
47
+
48
+ import { existsSync, readFileSync } from "node:fs";
49
+ import { join, resolve as resolvePath } from "node:path";
50
+
51
+ import { DEFAULT_RK_SERVER_URL, ENV_VAR } from "../constants";
52
+
53
+ export interface ResolveServerUrlOptions {
54
+ /** Explicit override (e.g. from `RkClientOptions.serverUrl`). */
55
+ serverUrl?: string;
56
+ /**
57
+ * Directory to start the `.roku/rk-server.json` walk from. Defaults
58
+ * to `process.cwd()`; tests inject a synthetic start dir so they
59
+ * don't depend on the actual filesystem layout.
60
+ */
61
+ startDir?: string;
62
+ }
63
+
64
+ export function resolveServerUrl(opts: ResolveServerUrlOptions = {}): string {
65
+ return normalizeServerUrl(resolveRawServerUrl(opts));
66
+ }
67
+
68
+ function resolveRawServerUrl(opts: ResolveServerUrlOptions): string {
69
+ if (opts.serverUrl) return opts.serverUrl;
70
+ const envUrl = process.env[ENV_VAR.RK_SERVER_URL];
71
+ if (envUrl) return envUrl;
72
+ const discovered = readRkServerDiscovery(opts.startDir ?? process.cwd());
73
+ if (discovered) return discovered;
74
+ return DEFAULT_RK_SERVER_URL;
75
+ }
76
+
77
+ /**
78
+ * Coerce any of the historical accepted shapes —
79
+ * `http://host:port`, `http://host:port/`, `http://host:port/api`,
80
+ * `http://host:port/api/` — into the canonical
81
+ * `http://host:port/api` (no trailing slash). Idempotent. Exported
82
+ * for unit tests; production callers should always go through
83
+ * `resolveServerUrl`.
84
+ */
85
+ export function normalizeServerUrl(url: string): string {
86
+ const trimmed = url.endsWith("/") ? url.slice(0, -1) : url;
87
+ return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`;
88
+ }
89
+
90
+ /**
91
+ * Best-effort read of `.roku/rk-server.json#apiEndpoint` by walking
92
+ * up from `startDir` and looking for the file directly. Returns
93
+ * `null` on any failure.
94
+ *
95
+ * We look for the FILE (`.roku/rk-server.json`) rather than its
96
+ * containing directory (`.roku/`) because per-suite scaffolding
97
+ * often creates a local placeholder `.roku/` (e.g. `device-tests/
98
+ * .roku/` is a gitkept empty dir). Anchoring on the file — which is
99
+ * only written by `rk server` — sidesteps those false positives.
100
+ *
101
+ * Exported for unit tests — production code should go through
102
+ * `resolveServerUrl`.
103
+ */
104
+ export function readRkServerDiscovery(startDir: string): string | null {
105
+ const file = findRkServerConfigFile(startDir);
106
+ if (!file) return null;
107
+ try {
108
+ const raw = readFileSync(file, "utf8");
109
+ const parsed = JSON.parse(raw) as { apiEndpoint?: unknown };
110
+ if (typeof parsed.apiEndpoint === "string" && parsed.apiEndpoint) {
111
+ return parsed.apiEndpoint;
112
+ }
113
+ return null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Walk up from `startDir` looking for a `.roku/rk-server.json` file.
121
+ * Returns the absolute path to the first match, or `null` if we
122
+ * reach `/` first without finding one. The walk stops only at the
123
+ * filesystem root — we do NOT treat `.git` / `pnpm-workspace.yaml`
124
+ * as fences, because `rk server` typically runs at the outermost
125
+ * monorepo root while tests run from a package-level cwd several
126
+ * levels below, and we want them to find the outer config.
127
+ */
128
+ function findRkServerConfigFile(startDir: string): string | null {
129
+ let dir = resolvePath(startDir);
130
+ for (;;) {
131
+ const candidate = join(dir, ".roku", "rk-server.json");
132
+ if (existsSync(candidate)) return candidate;
133
+ const parent = resolvePath(dir, "..");
134
+ if (parent === dir) return null;
135
+ dir = parent;
136
+ }
137
+ }
@@ -0,0 +1,520 @@
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
+ * Path to a module that default-exports (or exports `createDeploymentDriver`)
19
+ * a factory returning a `DeploymentDriver`. When set, `createDefaultClient`
20
+ * runs apps through the driver instead of a device and needs no device IP.
21
+ */
22
+ RK_DEVICE_DRIVER_MODULE: "RK_DEVICE_DRIVER_MODULE",
23
+ /**
24
+ * Roku developer-mode password. Set together with `RK_DEVICE_IP`
25
+ * to bypass `.roku/roku-dev-config.json`. A partial pair is a
26
+ * misconfiguration and fails loudly with `DeployFailedError`
27
+ * (silent fallback would mask the partial-config case).
28
+ */
29
+ RK_DEVICE_PASSWORD: "RK_DEVICE_PASSWORD",
30
+ /**
31
+ * Absolute path override for the baselines root directory. Primary
32
+ * mechanism is `setBaselineRoot()`; this env var is the one-off
33
+ * escape hatch for scripts run outside the configured Jest setup.
34
+ */
35
+ DEVICE_TEST_BASELINES: "DEVICE_TEST_BASELINES",
36
+ /**
37
+ * Resolution override (`"hd"` | `"fhd"`) for baseline-directory
38
+ * selection when no active `RkClient` has a cached `DeviceInfo`
39
+ * (e.g. ad-hoc CLI use). Ignored once a client has been registered
40
+ * via `setActiveClient()`.
41
+ */
42
+ DEVICE_TEST_RESOLUTION: "DEVICE_TEST_RESOLUTION",
43
+ /**
44
+ * Base URL of the `rk server` daemon. Canonical shape is
45
+ * `http://host:port/api`; bare `http://host:port` is also accepted
46
+ * (normalizer appends `/api`). Defaults to `DEFAULT_RK_SERVER_URL`.
47
+ */
48
+ RK_SERVER_URL: "RK_SERVER_URL",
49
+ /**
50
+ * Per-call idle timeout (ms) for the `POST /api/build` SSE stream.
51
+ * Caps sidecar wedges at a recoverable per-call failure instead
52
+ * of eating the whole CI job. Override when cold esbuild init on
53
+ * constrained CI runners exceeds the default
54
+ * `BUILD_SSE_IDLE_TIMEOUT_DEFAULT_MS`. Invalid values warn and
55
+ * fall back to the default — startup must never crash on a typo.
56
+ */
57
+ BUILD_SSE_IDLE_TIMEOUT_MS: "RK_BUILD_SSE_IDLE_TIMEOUT_MS",
58
+ /**
59
+ * Identifier for the GitLab CI shard / job slice running this
60
+ * suite (e.g. "runtime", "components"). Surfaced in JUnit
61
+ * `<properties>` and the `[device-lease]` log so post-hoc
62
+ * telemetry can correlate per-shard wall time and device usage.
63
+ *
64
+ * Defaults to `"default"` when unset so JUnit consumers and
65
+ * dashboards never render an empty value (an empty value is
66
+ * indistinguishable from a missing property in most viewers).
67
+ *
68
+ * Forms part of the parallel-device telemetry baseline and is
69
+ * consumed verbatim by the parallel-shard JUnit naming. Defining
70
+ * it here lets the env-allowlist reference the same name without
71
+ * churn.
72
+ */
73
+ RK_SHARD_NAME: "RK_SHARD_NAME",
74
+ /**
75
+ * Identifier for the device-pool provider that supplied the
76
+ * device for this run (e.g. `"local"`, `"cloud"`).
77
+ * Surfaced in JUnit `<properties>` and the `[device-lease]`
78
+ * log so telemetry can attribute regressions to a specific
79
+ * provider tier.
80
+ *
81
+ * **Forward-compat note.** The original spec called for a
82
+ * hardcoded `"local"` literal at this stage. We plumb an env
83
+ * var with a `"local"` default instead so:
84
+ * - Baseline runs emit the same value (no behavioral
85
+ * diff against the spec).
86
+ * - Provider-backed pools can set this
87
+ * env var from their CLI without retrofitting
88
+ * `device-test-client`.
89
+ *
90
+ * If the provider seam ends up choosing a different injection
91
+ * mechanism (e.g. `ResolvedConfig.pool` via a runtime setter),
92
+ * this env var becomes dead and should be removed.
93
+ */
94
+ RK_PROVIDER_KIND: "RK_PROVIDER_KIND",
95
+ /**
96
+ * ULID of the device-pool lease the current test process should
97
+ * read its target IP + passwordRef from. When set, the resolver's
98
+ * **Layer 0** reads `.roku/leases/<id>.json`, Zod-validates it
99
+ * through `DeviceLeaseSchema`, and returns the embedded `ip` /
100
+ * resolved `passwordRef`.
101
+ *
102
+ * Precedence inside the resolver (later overrides earlier):
103
+ * 1. (Layer 0) `RK_DEVICE_LEASE_ID` — set by `rk device lease
104
+ * acquire`; the canonical CI path for provider-backed pools.
105
+ * 2. `RK_DEVICE_IP` — partner-side explicit override; STILL
106
+ * wins on conflict so we don't surprise partner pipelines.
107
+ * 3. `.roku/` active device → first registered device (today's
108
+ * behavior).
109
+ *
110
+ * Forms the producer side of the parallel-CI model. The acquiring
111
+ * provider writes the lease file; this env var pins
112
+ * the consumer at boot to the correct lease without reading
113
+ * any workspace-singleton state (`.roku/roku-dev-config.json`
114
+ * cannot be safely shared across parallel jobs).
115
+ */
116
+ RK_DEVICE_LEASE_ID: "RK_DEVICE_LEASE_ID",
117
+
118
+ /**
119
+ * Directory holding lease files, defaulting to `<cwd>/.roku/leases`.
120
+ * Set by CI (and `scripts/run-shards-on-devices.sh`) so a job running
121
+ * from a subdirectory still finds the lease the acquire step wrote.
122
+ *
123
+ * Every reader must resolve it identically — two readers disagreeing on
124
+ * the directory silently read two different leases.
125
+ */
126
+ RK_DEVICE_LEASE_DIR: "RK_DEVICE_LEASE_DIR"
127
+ } as const;
128
+
129
+ /** Default delay between successive ECP keypresses, in milliseconds. */
130
+ export const DEFAULT_KEY_DELAY = 100;
131
+
132
+ /**
133
+ * Default per-request ECP timeout for non-capture calls. 10s (not 5s)
134
+ * because `/launch/dev` against an Instant-Resume manifest can take
135
+ * several seconds on slow hardware before returning 200.
136
+ */
137
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
138
+
139
+ /**
140
+ * Retry backoff after a transient ECP failure (5xx or timeout). Callers
141
+ * see a single retry; if the second attempt fails the original error
142
+ * propagates.
143
+ */
144
+ export const ECP_RETRY_BACKOFF_MS = 500;
145
+
146
+ /**
147
+ * Per-category minimum gap (ms) between the END of the previous call
148
+ * and the START of the next. `app-ui` 200ms (waitForNode cadence),
149
+ * `capture` 500ms (expensive); other categories 0 (still serialized).
150
+ */
151
+ export const ECP_INTERVAL_MS: Readonly<Record<EcpCategory, number>> = {
152
+ "query-appui": 200,
153
+ "query-device-info": 0,
154
+ "query-active-app": 0,
155
+ keypress: 0,
156
+ launch: 0,
157
+ capture: 500
158
+ };
159
+
160
+ /** Video modes the framework recognises. Anything else fails fast at startup. */
161
+ export const SUPPORTED_VIDEO_MODES: ReadonlySet<string> = new Set(["720p", "720p60", "1080p", "1080p60"]);
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Device recovery (crash / reboot resilience)
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Default budget for `waitForDeviceOnline`. Roku crash-recovery
169
+ * (reboot + home-screen mount + ECP responder) lands in 60–120s on
170
+ * reference hardware; 180s covers cold-boot on Roku Express without
171
+ * letting a permanent outage hang silently.
172
+ */
173
+ export const DEVICE_ONLINE_DEFAULT_TIMEOUT_MS = 180_000;
174
+
175
+ /**
176
+ * Probe interval inside `waitForDeviceOnline`. Sub-second cadence has
177
+ * been observed to starve `/logs/*` transitions on the rk-server side
178
+ * (single-threaded ECP responder during the crash window).
179
+ */
180
+ export const DEVICE_ONLINE_POLL_INTERVAL_MS = 2_000;
181
+
182
+ /**
183
+ * Default budget for the installer-port (HTTP/80) phase of
184
+ * `waitForDeviceFullyReady`. The installer is what `rk` POSTs the
185
+ * sideload package to, and on reference hardware it lags ECP recovery
186
+ * by a few seconds after a crash/reboot — long enough that probing only
187
+ * `/query/device-info` (port 8060) and immediately retrying a deploy
188
+ * yields the misleading "Device is not reachable on the network"
189
+ * diagnostic. 60s covers the observed lag with headroom while still
190
+ * keeping the overall guard budget bounded.
191
+ */
192
+ export const INSTALLER_PORT_PROBE_DEFAULT_TIMEOUT_MS = 60_000;
193
+
194
+ /**
195
+ * Probe interval for the installer-port arm of
196
+ * `waitForDeviceFullyReady`. 1s is fast enough to catch the typical
197
+ * 2–10s post-ECP installer recovery window without flooding the device
198
+ * with TCP SYNs while it is still booting.
199
+ */
200
+ export const INSTALLER_PORT_PROBE_INTERVAL_MS = 1_000;
201
+
202
+ /**
203
+ * Per-attempt TCP-connect timeout for the installer-port probe. Mirrors
204
+ * `tcpProbe`'s default in `@roku-sdk/tooling`; surfaced here so tests
205
+ * and callers can reason about probe latency without crossing package
206
+ * boundaries.
207
+ */
208
+ export const INSTALLER_PORT_TCP_TIMEOUT_MS = 1_500;
209
+
210
+ /**
211
+ * Test-level retries for device recovery. Higher counts mask real
212
+ * regressions; lower counts leave first-observed crashes unrecovered.
213
+ */
214
+ export const DEVICE_RECOVERY_DEFAULT_RETRIES = 3;
215
+
216
+ /**
217
+ * Consecutive deploys that may fail to foreground the channel on a VM
218
+ * known to have foregrounded one earlier before the wedge is treated as
219
+ * fatal rather than as a bad launch.
220
+ *
221
+ * One file's full deploy-retry budget is allowed to fail softly, so a
222
+ * genuinely transient stall still resolves the way it always has; the
223
+ * next failure after that is a second file reaching the same verdict,
224
+ * which no amount of further retrying has ever cured. Derived from the
225
+ * retry budget so the two cannot drift apart.
226
+ */
227
+ export const FOREGROUND_FAILURE_STREAK_FATAL = DEVICE_RECOVERY_DEFAULT_RETRIES + 1;
228
+
229
+ /**
230
+ * Default post-sideload readiness deadline. Covers cold-start on the
231
+ * slowest reference hardware (Express ~3s, Ultra ~1s) with headroom
232
+ * for GC/HDCP stalls. Pass `readinessTimeout` explicitly for slower fixtures.
233
+ */
234
+ export const READINESS_TIMEOUT_MS = 10_000;
235
+
236
+ /**
237
+ * Sleep between readiness probes on the `/query/active-app` arm
238
+ * (which has a 0ms queue gap). Avoids DoS-ing the device during
239
+ * sideload transitions.
240
+ */
241
+ export const READINESS_POLL_INTERVAL_MS = 200;
242
+
243
+ /**
244
+ * Consecutive predicate passes before declaring ready. Two catches
245
+ * crashes shortly after first render (observed `EXIT_APP_ERROR` ~300ms
246
+ * after splash); higher counts add deploy latency for diminishing
247
+ * returns since later `assertAppAlive` calls cover longer windows.
248
+ */
249
+ export const READINESS_CONFIRM_POLLS = 2;
250
+
251
+ /**
252
+ * Fraction of the readiness budget tolerating post-launch home-screen
253
+ * responses before a previously-observed app is treated as
254
+ * `crashed-after-ready`. `/launch/dev` returns 200 before firmware
255
+ * swaps the active app (~200–500ms transition), and InstantOn can
256
+ * briefly bounce through Home after first render. A never-observed app
257
+ * is classified only at the full readiness timeout; slow cold sideloads
258
+ * can remain on Home longer than this grace window and still launch.
259
+ */
260
+ export const HOME_SCREEN_GRACE_FRACTION = 0.25;
261
+
262
+ /**
263
+ * Absolute floor for the home-screen grace window. Without it, a
264
+ * sub-second readiness budget would compute a grace below the
265
+ * `/launch/dev` → off-home transition and false-positive.
266
+ */
267
+ export const HOME_SCREEN_GRACE_MIN_MS = 500;
268
+
269
+ /**
270
+ * Launch requests the readiness gate re-sends, per deploy, when the
271
+ * device log shows the channel entering and then exiting with
272
+ * `EXIT_APP_ERROR`. Firmware 15.3.4 on virtualized hosts ends some
273
+ * launches that way right after a sideload, its TypeScript process still
274
+ * compiling the build `replace()` removed, and a launch of the same build
275
+ * a few seconds later succeeds. A build that crashes during mount exits
276
+ * the same way and pays at most this many extra launches per deploy.
277
+ */
278
+ export const LAUNCH_CRASH_RELAUNCH_MAX = 2;
279
+
280
+ /**
281
+ * Minimum spacing between two launch requests from the readiness gate.
282
+ * A relaunch also waits out the home-screen grace window, so InstantOn's
283
+ * own exit-and-relaunch is not raced.
284
+ */
285
+ export const LAUNCH_CRASH_RELAUNCH_GAP_MS = 2_000;
286
+
287
+ /**
288
+ * Tag names of root-level system dialogs (e.g. "Display type detection").
289
+ * Matching on tag at the root rules out app-authored `Dialog` subclasses
290
+ * (those live inside a mounted Scene, not as root siblings).
291
+ */
292
+ export const SYSTEM_DIALOG_TAGS: ReadonlySet<string> = new Set([
293
+ "Dialog",
294
+ "StandardMessageDialog",
295
+ "StandardDialog",
296
+ "ProblemDialog",
297
+ "KeyboardDialog",
298
+ "PinEntryDialog"
299
+ ]);
300
+
301
+ /**
302
+ * Default `waitForNode` cadence; matches the `/query/app-ui` queue gap
303
+ * so polling lands naturally on the queue's rhythm.
304
+ */
305
+ export const WAIT_FOR_NODE_DEFAULT_INTERVAL_MS = 200;
306
+
307
+ /** Max serialized depth in `summarizeTree`; deeper subtrees become `[N children]`. */
308
+ export const TREE_SUMMARY_MAX_DEPTH = 3;
309
+
310
+ /** Hard cap on `summarizeTree` output length. */
311
+ export const TREE_SUMMARY_MAX_CHARS = 2000;
312
+
313
+ /** Snippet length for expected/actual JSON values in `TreeMismatchError`. */
314
+ export const TREE_DIFF_SNIPPET_MAX_CHARS = 200;
315
+
316
+ /** Baseline directory name per-resolution. */
317
+ export const BASELINE_RESOLUTION_DIR = {
318
+ hd: "hd",
319
+ fhd: "fhd"
320
+ } as const;
321
+
322
+ /** Build output directory the client uses for fixture apps, relative to the app root. */
323
+ export const INTEGRATION_BUILD_OUT_DIR = "dist-integration-test";
324
+
325
+ /** Filenames written under each consumer's `test-results/` directory. */
326
+ export const ARTIFACT_FILE = {
327
+ DEVICE_ENV: "device-env.json",
328
+ JUNIT: "junit.xml",
329
+ DEVICE_LOG: "device.log",
330
+ /**
331
+ * CommonJS file written by the telemetry helper that the
332
+ * `jest-junit` reporter `require()`s at JUnit generation
333
+ * time to add per-`<testcase>` `<properties>` entries
334
+ * (`device-id`, `shard-name`, `providerKind`). Picked up via
335
+ * `jest-junit`'s `testCasePropertiesFile` /
336
+ * `testCasePropertiesDirectory` options. Filename is exposed
337
+ * as a constant so the consumer's `jest.config.ts` and the
338
+ * writer in `telemetry.ts` reference the same string.
339
+ */
340
+ JUNIT_CASE_PROPERTIES: "junit-test-case-properties.js"
341
+ } as const;
342
+
343
+ /**
344
+ * Optional `.passthrough()` riders on `.roku/leases/<id>.json` that this
345
+ * package reads. Not declared by `DeviceLeaseSchema`, so the names are the
346
+ * whole contract — they must match the provider that stamps them (see the
347
+ * "Known passthrough riders" list in
348
+ * `@roku-sdk/tooling/src/device-pool/schemas/lease.ts`).
349
+ */
350
+ export const LEASE_FIELD = {
351
+ /**
352
+ * OS image the pool provider flashed the device with before handing out
353
+ * the lease. Recorded verbatim in `device-env.json` so a failed run can
354
+ * be traced back to an image: the ECP `firmwareVersion` is too coarse
355
+ * (several images report the same `ui-software` version) and the
356
+ * provider-side libraries (`grandcentral`, `trc-channel`) can differ
357
+ * between two devices that report identical firmware.
358
+ */
359
+ ROOTFS_ARTIFACTORY_PATH: "rootfsArtifactoryPath"
360
+ } as const;
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // Failure diagnostics
364
+ // ---------------------------------------------------------------------------
365
+
366
+ /** Directory (relative to `testResultsDir`) for per-failure bundles. */
367
+ export const FAILURES_DIR = "failures";
368
+
369
+ /**
370
+ * Filenames inside each per-failure bundle. Keep in sync with
371
+ * `writeFailureBundle` — CI artifact pipelines key off these names.
372
+ */
373
+ export const FAILURE_ARTIFACT_FILE = {
374
+ SCREENSHOT: "screenshot.png",
375
+ DOM_TREE: "dom-tree.json",
376
+ RECENT_LOGS: "recent-logs.txt",
377
+ ERROR: "error.txt",
378
+ /**
379
+ * `LogStreamHealth` snapshot at failure time; lets a reviewer
380
+ * attribute a log timeout to either the device→server telnet leg
381
+ * or the server→client SSE leg without rerunning.
382
+ */
383
+ STREAM_HEALTH: "stream-health.json"
384
+ } as const;
385
+
386
+ /** Default tail size for `recent-logs.txt`. */
387
+ export const FAILURE_RECENT_LOG_LINES = 100;
388
+
389
+ /**
390
+ * Global keys shared between consumer `setup.ts` and the custom Jest
391
+ * test environment. Globals (not env options) because Jest constructs
392
+ * the environment in a different module context from `setup.ts` and
393
+ * globals are the only surface both sides reach.
394
+ */
395
+ export const DEVICE_ENV_GLOBAL_KEY = {
396
+ CLIENT: "__DEVICE_CLIENT__",
397
+ TEST_RESULTS_DIR: "__DEVICE_TEST_RESULTS_DIR__",
398
+ LOG_SINK: "__DEVICE_LOG_SINK__",
399
+ /**
400
+ * Opt-out switch for the failure-bundle pipeline. Written
401
+ * synchronously at install time (not in `beforeAll`) so the first
402
+ * `test_fn_failure` event already sees the final value. Absent
403
+ * key + absent `TEST_RESULTS_DIR` → env stays silent (vanilla
404
+ * `jest-environment-node` consumers don't get unsolicited bundles).
405
+ */
406
+ CAPTURE_ENABLED: "__DEVICE_CAPTURE_ENABLED__"
407
+ } as const;
408
+
409
+ /**
410
+ * Per-suite cache of the IP resolved by `globalSetup`. Workers read
411
+ * synchronously from `createDefaultClient()` since `listMergedDevices`
412
+ * is async and the worker-side factory is sync.
413
+ */
414
+ export const DEVICE_IP_CACHE_FILE = ".device-ip";
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Log assertions
418
+ // ---------------------------------------------------------------------------
419
+
420
+ /**
421
+ * Default `rk server` base URL. Canonical form includes `/api` (matches
422
+ * `apiEndpoint` in `.roku/rk-server.json`); bare `http://host:port` is
423
+ * normalized by `resolveServerUrl`.
424
+ */
425
+ export const DEFAULT_RK_SERVER_URL = "http://localhost:3737/api";
426
+
427
+ /** Roku device console log port. Mirror of `tooling-utils/constants.ts → ROKU_LOG_PORT`. */
428
+ export const ROKU_LOG_PORT = 8085;
429
+
430
+ /**
431
+ * Hard cap on retained log entries per worker. ~10k × ~200 B ≈ 2MB
432
+ * keeps a long-running suite within memory bounds; an earlier 200k
433
+ * cap drove the device-test container past its cgroup limit.
434
+ */
435
+ export const LOG_BUFFER_MAX_LINES = 10_000;
436
+
437
+ /**
438
+ * Oldest entries dropped on cap. Half-drop is amortised O(1) per
439
+ * append vs drop-one-per-append; the doubled retention window absorbs
440
+ * short `since`-pos snapshots without surprise overflows.
441
+ */
442
+ export const LOG_BUFFER_DROP_HALF = 5_000;
443
+
444
+ /** Default `waitForLog` timeout. */
445
+ export const WAIT_FOR_LOG_DEFAULT_TIMEOUT_MS = 5_000;
446
+
447
+ /** Upper bound on `recentLines` rendered into `WaitForLogTimeoutError`. */
448
+ export const WAIT_FOR_LOG_RECENT_LINES_MAX = 20;
449
+
450
+ /** Character cap on the recentLines preview block. */
451
+ export const WAIT_FOR_LOG_RECENT_CHARS_MAX = 600;
452
+
453
+ /**
454
+ * SSE parser `pending` accumulator cap. Frames are normally a few
455
+ * hundred bytes; a stream past 1 MiB without `\n\n` is broken or
456
+ * hostile — fail loud rather than leak memory.
457
+ */
458
+ export const SSE_PENDING_MAX_BYTES = 1_048_576;
459
+
460
+ /**
461
+ * Grace window granted to a log assertion whose wait window stayed
462
+ * empty, measured from the re-attach it triggers. `/logs/tail`
463
+ * resolves readiness on its first status frame and emits the
464
+ * replayed buffer immediately after, so the lines land a round trip
465
+ * — not a device operation — later.
466
+ */
467
+ export const LOG_STREAM_RESYNC_GRACE_MS = 1_000;
468
+
469
+ /**
470
+ * Default per-call idle timeout (ms) for the `POST /api/build` SSE
471
+ * stream consumed by `requestServerBuild`. Sized for the worst-case
472
+ * cold path on constrained CI runners: first-time esbuild service
473
+ * spawn (~10–15s on shared CI containers) + Hermes warmup
474
+ * (~30–60s) + first incremental TS resolve (~30s). 90s — the prior
475
+ * value — could trip on cold starts and surface as the opaque
476
+ * `RK_SERVER_BUILD_IDLE_TIMEOUT` code, indistinguishable from a
477
+ * real build error. 180s comfortably covers cold-start without
478
+ * letting a permanent sidecar wedge eat the whole job's budget.
479
+ *
480
+ * Override per-environment via the `RK_BUILD_SSE_IDLE_TIMEOUT_MS`
481
+ * env var (any positive integer, in ms). Invalid values warn and
482
+ * fall back to this default rather than crashing the test run.
483
+ */
484
+ export const BUILD_SSE_IDLE_TIMEOUT_DEFAULT_MS = 180_000;
485
+
486
+ // ---------------------------------------------------------------------------
487
+ // Screenshot assertions
488
+ // ---------------------------------------------------------------------------
489
+
490
+ /**
491
+ * Default convergence interval for `captureStableScreenshot`. Matches
492
+ * the ECP queue's `capture` gap so polling lands on the queue's cadence.
493
+ */
494
+ export const SCREENSHOT_STABLE_DEFAULT_INTERVAL_MS = 500;
495
+
496
+ /**
497
+ * Default pixel-mismatch ratio for `expectScreenshot` (1% of the
498
+ * unmasked canvas). Tolerates anti-aliasing drift without missing
499
+ * legitimate content changes.
500
+ */
501
+ export const DEFAULT_SCREENSHOT_THRESHOLD = 0.01;
502
+
503
+ /**
504
+ * Per-pixel YIQ cutoff for `pixelmatch`. Decoupled from the caller-facing
505
+ * threshold (which is the aggregate-ratio knob); 0.1 is pixelmatch's
506
+ * own library default.
507
+ */
508
+ export const PIXELMATCH_COLOR_THRESHOLD = 0.1;
509
+
510
+ /**
511
+ * Per-axis ceiling for `ScreenshotMatchOptions.alignTolerance`. The
512
+ * search is O((2x+1)(2y+1)) candidate offsets, so the cap bounds both
513
+ * the runtime and how far the comparator may slide a capture before
514
+ * scoring it — a wide window could "align" genuinely different content.
515
+ * Observed device-dependent OSD re-centering is 1-3px.
516
+ */
517
+ export const ALIGN_TOLERANCE_MAX_PX = 4;
518
+
519
+ /** Default diff-PNG directory; overridable via `setScreenshotDiffRoot`. */
520
+ export const SCREENSHOT_DIFF_DIR = "test-results/diffs";