@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
package/src/types.ts ADDED
@@ -0,0 +1,472 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ import type { DeviceTarget } from "@roku-sdk/tooling/src/tooling-utils/device-target";
4
+
5
+ export interface DeviceInfo {
6
+ modelName: string;
7
+ modelNumber: string;
8
+ modelFamily: string;
9
+ deviceClass: string;
10
+ firmwareVersion: string;
11
+ videoMode: string;
12
+ locale: string;
13
+ country: string;
14
+ libraries: Record<string, string>;
15
+ }
16
+
17
+ export interface AppUiNode {
18
+ /** XML tag name (e.g. `"Label"`, `"RowList"`). */
19
+ tag: string;
20
+ /**
21
+ * Raw ECP attributes verbatim (all values are strings; numeric
22
+ * comparisons require parsing). `bounds`/`focused` are also kept
23
+ * here as strings so attribute iteration is exhaustive.
24
+ */
25
+ attrs: Record<string, string>;
26
+ bounds: { x: number; y: number; width: number; height: number };
27
+ focused: boolean;
28
+ children: AppUiNode[];
29
+ }
30
+
31
+ export interface NodePredicate {
32
+ tag?: string;
33
+ attr?: Record<string, string>;
34
+ focused?: boolean;
35
+ }
36
+
37
+ export interface LibraryRequirement {
38
+ /** Library identifier as reported by Roku ECP (`rida`, etc.). */
39
+ name: string;
40
+ /** Semver range (e.g. `">=2.0"`) — matched against the library's reported version. */
41
+ version: string;
42
+ }
43
+
44
+ /**
45
+ * Test-declared requirements for `requirePrecondition` / `itOnDevice` /
46
+ * `describeOnDevice`.
47
+ *
48
+ * Match strategy: `firmwareVersion` semver range (4-part Roku versions
49
+ * coerced to MAJOR.MINOR.PATCH); `modelFamily` exact;
50
+ * `deviceClass` exact against a derived perf tier (NOT `DeviceInfo.deviceClass`,
51
+ * which is form-factor); `videoMode` prefix; `locale`/`country` exact.
52
+ * `library` throws `PreconditionNotImplementedError` (no ECP query path).
53
+ *
54
+ * Evaluation is in key-insertion order, so the caller controls which
55
+ * field produces the error when multiple mismatch.
56
+ */
57
+ export interface Precondition {
58
+ firmwareVersion?: string;
59
+ modelFamily?: string;
60
+ deviceClass?: string;
61
+ videoMode?: string;
62
+ locale?: string;
63
+ country?: string;
64
+ library?: LibraryRequirement;
65
+ }
66
+
67
+ export interface MaskRegion {
68
+ x: number;
69
+ y: number;
70
+ width: number;
71
+ height: number;
72
+ }
73
+
74
+ export interface WaitOptions {
75
+ timeout: number;
76
+ interval?: number;
77
+ }
78
+
79
+ export interface CaptureStableOptions {
80
+ timeout: number;
81
+ interval?: number;
82
+ mask?: MaskRegion[];
83
+ /**
84
+ * Maximum number of differing pixels (outside `mask`) between two
85
+ * consecutive captures that still counts as "stable". Defaults to
86
+ * `0` — strict pixel equality, the historical contract.
87
+ *
88
+ * Use a small positive value when the firmware's anti-aliasing
89
+ * pipeline emits sub-pixel jitter on static text (~50–500 pixels
90
+ * per frame on FHD) even after the user-visible content has
91
+ * settled. Pair with `expectScreenshot(...).toMatchBaseline(..., {
92
+ * threshold })` whose `threshold` covers the same tolerance — the
93
+ * stability gate must not be stricter than the assertion gate,
94
+ * otherwise `captureStableScreenshot` can time out on a screen
95
+ * the baseline would have accepted.
96
+ */
97
+ tolerance?: number;
98
+ }
99
+
100
+ export interface KeySequenceOptions {
101
+ keyDelay?: number;
102
+ }
103
+
104
+ export interface WaitForDeviceOnlineOptions {
105
+ /** Total wait budget; defaults to `DEVICE_ONLINE_DEFAULT_TIMEOUT_MS` (180s). */
106
+ timeoutMs?: number;
107
+ /** Probe interval; defaults to `DEVICE_ONLINE_POLL_INTERVAL_MS` (2s). */
108
+ intervalMs?: number;
109
+ }
110
+
111
+ /**
112
+ * Options for `RkClient.waitForDeviceFullyReady`. The method gates on
113
+ * BOTH the ECP responder (port 8060, via `/query/device-info`) AND the
114
+ * sideload installer (port 80, via TCP connect). Each phase has an
115
+ * independent budget so a slow installer cannot starve the ECP probe
116
+ * (and vice versa); the overall wall time is bounded by their sum.
117
+ *
118
+ * Why two phases: the original `waitForDeviceOnline` only proves the
119
+ * device is responding on ECP. After a crash/reboot the installer (the
120
+ * port `rk` POSTs the sideload package to) lags ECP recovery by several
121
+ * seconds; retrying a deploy in that window surfaces the misleading
122
+ * "Device is not reachable on the network" diagnostic from
123
+ * `sideload-diagnostics`. Probing both before the next deploy attempt
124
+ * eliminates that whole failure class — see `installDeployGuard`.
125
+ */
126
+ export interface WaitForDeviceFullyReadyOptions {
127
+ /**
128
+ * Budget for the ECP `/query/device-info` arm. Defaults to
129
+ * `DEVICE_ONLINE_DEFAULT_TIMEOUT_MS` (180s). Forwarded verbatim to
130
+ * the underlying `waitForDeviceOnline` call.
131
+ */
132
+ ecpTimeoutMs?: number;
133
+ /**
134
+ * Probe interval for the ECP arm. Defaults to
135
+ * `DEVICE_ONLINE_POLL_INTERVAL_MS` (2s).
136
+ */
137
+ ecpIntervalMs?: number;
138
+ /**
139
+ * Budget for the installer-port (TCP/80) arm, applied AFTER the ECP
140
+ * arm resolves. Defaults to `INSTALLER_PORT_PROBE_DEFAULT_TIMEOUT_MS`
141
+ * (60s).
142
+ */
143
+ installerTimeoutMs?: number;
144
+ /**
145
+ * Probe interval for the installer-port arm. Defaults to
146
+ * `INSTALLER_PORT_PROBE_INTERVAL_MS` (1s).
147
+ */
148
+ installerIntervalMs?: number;
149
+ }
150
+
151
+ export type LogPosition = string;
152
+
153
+ export type LogLine = string;
154
+
155
+ export interface LogQueryOptions {
156
+ since: LogPosition;
157
+ timeout?: number;
158
+ window?: number;
159
+ }
160
+
161
+ export interface LaunchOptions {
162
+ /**
163
+ * ECP channel id to launch. Defaults to `"dev"` (the currently
164
+ * sideloaded development channel). Published channels use their
165
+ * numeric id (e.g. `"12345"`).
166
+ */
167
+ channelId?: string;
168
+ /**
169
+ * Deep-link params serialized into the launch URL. Reliable on
170
+ * `launch()` (the runtime is already subscribed); cold sideloads
171
+ * must use `buildAndLaunchWithParams` instead.
172
+ */
173
+ deepLink?: Record<string, string>;
174
+ /**
175
+ * Post-launch readiness budget; defaults to `READINESS_TIMEOUT_MS`.
176
+ * On timeout: `DeployFailedError`, device flagged unhealthy.
177
+ */
178
+ readinessTimeout?: number;
179
+ /**
180
+ * Semantic readiness gate. See `BuildAndDeployOptions.readyNode`
181
+ * for picking-the-predicate guidance.
182
+ */
183
+ readyNode?: NodePredicate;
184
+ }
185
+
186
+ export interface BuildAndDeployOptions {
187
+ /**
188
+ * Deep-link params for the post-sideload `/launch/dev?…` POST.
189
+ *
190
+ * Cold-mount race: the POST frequently lands before the runtime
191
+ * subscribes to `setLaunchParamsUpdateHandler`, and firmware
192
+ * silently drops it. Readiness passing is NOT evidence the params
193
+ * arrived. Use this only for apps that ignore params, read them
194
+ * synchronously on mount, or gate on a fixture-emitted log line.
195
+ * For cold sideloads that must land in a deep-linked state, use
196
+ * `buildAndLaunchWithParams` instead.
197
+ */
198
+ deepLink?: Record<string, string>;
199
+ /** Readiness budget; defaults to `READINESS_TIMEOUT_MS` (10s). */
200
+ readinessTimeout?: number;
201
+ /** ECP channel id to launch after sideload; defaults to `"dev"`. */
202
+ channelId?: string;
203
+ /** Override device selector; defaults to active `.roku/` device. */
204
+ deviceSelector?: string;
205
+ /**
206
+ * Semantic readiness gate: a predicate matching a user-rendered
207
+ * element. Without it, readiness fires on the SceneGraph skeleton
208
+ * (mounted before user code), so `buildAndDeploy` can return
209
+ * "success" while `main()` is still compiling or crashing during
210
+ * mount (`EXIT_APP_ERROR` 0.3–1.0s after splash). Pick a stable
211
+ * Label with a known id+text; avoid wrapper tags like `Group`.
212
+ *
213
+ * Once matched, readiness still requires two consecutive passing
214
+ * polls — catches "mounted then immediately crashed" as
215
+ * `DeployFailedError({ reason: "crashed-after-ready" })`.
216
+ * Omitting falls back to `tree.children.length > 0`.
217
+ */
218
+ readyNode?: NodePredicate;
219
+ /**
220
+ * Override esbuild's `drop: ["console", "debugger"]` pass:
221
+ * `true` always drops (perf profiling), `false` never drops (for
222
+ * fixtures that exercise console behaviour, e.g. polyfill
223
+ * verifiers), `undefined` falls back to the historical default
224
+ * (drop on `rk build`, keep on `rk dev`).
225
+ */
226
+ dropConsole?: boolean;
227
+ /**
228
+ * Build with development artifacts — no minification, readable identifiers,
229
+ * source maps. Equivalent to `rk build --debug`. Useful for profiling sessions
230
+ * where signal/effect names and stack frames remain human-readable.
231
+ */
232
+ dev?: boolean;
233
+ /**
234
+ * Enable SolidJS dev mode — imports the `solid-js/dev` build so signal
235
+ * and effect names remain readable in Perfetto tracepoints. Equivalent to
236
+ * `rk build --solid-dev-mode` or `buildVariables: { __SIGNAL__: true }` in
237
+ * `roku-config.ts`.
238
+ */
239
+ solidDevMode?: boolean;
240
+ /**
241
+ * Build-time variable overrides merged on top of the app's `roku-config.ts`
242
+ * `buildVariables`. Pass `{ __SIGNAL__: true }` to enable SolidJS dev mode
243
+ * (readable signal/effect names in Perfetto tracepoints).
244
+ */
245
+ buildVariables?: Record<string, boolean | string | number>;
246
+ }
247
+
248
+ /**
249
+ * Options for `buildAndLaunchWithParams`. Both log regexes are
250
+ * required: omitting either re-introduces the cold-mount race the
251
+ * primitive exists to avoid.
252
+ */
253
+ export interface BuildAndLaunchWithParamsOptions {
254
+ /** Device selector for phase-1 `buildAndDeploy`. */
255
+ deviceSelector?: string;
256
+ /** ECP channel id for both phases; defaults to `"dev"`. */
257
+ channelId?: string;
258
+ /** Readiness timeout forwarded to BOTH phases. */
259
+ readinessTimeout?: number;
260
+ /** Phase-1 semantic readiness gate; see `BuildAndDeployOptions.readyNode`. */
261
+ readyNode?: NodePredicate;
262
+ /**
263
+ * Required. Log regex emitted once the runtime's launch-params
264
+ * subscription is registered (e.g. an effect reading
265
+ * `useLaunchConfig()` for the first time). Gating phase 2 on this
266
+ * prevents the params POST from racing handler registration.
267
+ */
268
+ runtimeReadyLogPattern: RegExp;
269
+ /** "Runtime ready" gate timeout; defaults to 5s. */
270
+ runtimeReadyTimeout?: number;
271
+ /**
272
+ * Required. Log regex emitted when phase-2 params are observed by
273
+ * the reactive graph. Gating on this separates signal-delivery
274
+ * failures from renderer regressions in subsequent DOM timeouts.
275
+ */
276
+ paramsAckLogPattern: RegExp;
277
+ /** "Params ack" gate timeout; defaults to 5s. */
278
+ paramsAckTimeout?: number;
279
+ /** Forwarded to phase-1; see `BuildAndDeployOptions.dropConsole`. */
280
+ dropConsole?: boolean;
281
+ }
282
+
283
+ /**
284
+ * Narrow projection of `AppUiNode` for system-dialog surfacing.
285
+ * `tag`/`title` are top-level for reporter convenience; `node` keeps
286
+ * the full underlying tree for deeper inspection.
287
+ */
288
+ export interface SystemDialogNode {
289
+ /** XML tag that matched the `SYSTEM_DIALOG_TAGS` heuristic. */
290
+ tag: string;
291
+ /** `attrs.title` on the node, if present (common on StandardMessageDialog). */
292
+ title: string;
293
+ /** Full underlying AppUiNode, for reporters that want context. */
294
+ node: AppUiNode;
295
+ }
296
+
297
+ /**
298
+ * Max whole-pixel translation the comparator may apply to the capture
299
+ * before scoring it against the baseline. Both axes default to 0
300
+ * (exact positional match) and are capped at
301
+ * `ALIGN_TOLERANCE_MAX_PX` — needing more than a few pixels means the
302
+ * baseline content is genuinely different, not merely offset.
303
+ *
304
+ * Intended for firmware-drawn surfaces (system OSD dialogs) whose
305
+ * internal row heights round differently per device, re-centering the
306
+ * whole surface by 1-3px. App-rendered content is positionally
307
+ * deterministic and must not use this.
308
+ */
309
+ export interface AlignTolerance {
310
+ x?: number;
311
+ y?: number;
312
+ }
313
+
314
+ export interface ScreenshotMatchOptions {
315
+ /**
316
+ * Max fraction of unmasked pixels that may differ; defaults to
317
+ * 0.01 (1%). Per-pixel color sensitivity (YIQ 0.1) is internal
318
+ * and decoupled — widening this does not loosen per-pixel matching.
319
+ */
320
+ threshold?: number;
321
+ mask?: MaskRegion[];
322
+ /**
323
+ * Allow the capture to be translated by up to this many pixels on
324
+ * each axis to absorb device-dependent re-centering. Only consulted
325
+ * after an untranslated comparison has already failed, so it can
326
+ * rescue a failing assertion but never relax a passing one.
327
+ */
328
+ alignTolerance?: AlignTolerance;
329
+ }
330
+
331
+ export interface TreeMatchOptions {
332
+ /** Attribute keys stripped recursively before serialization. */
333
+ exclude?: readonly string[];
334
+ /**
335
+ * Node tags serialized as leaves: the node itself is asserted, its
336
+ * children are not. Use for native nodes whose internal chrome the
337
+ * app does not own and firmware restructures between builds.
338
+ */
339
+ excludeChildrenOf?: readonly string[];
340
+ }
341
+
342
+ /**
343
+ * `/query/active-app` result. Every field stays faithful to the XML, so
344
+ * decide "is this the home screen?" with `isHomeScreenActiveApp` rather
345
+ * than comparing fields directly — firmware ships several distinct home
346
+ * surfaces and no single field identifies all of them.
347
+ */
348
+ export interface ActiveAppInfo {
349
+ /**
350
+ * `"dev"` for sideloaded, channel id for published. Empty on the
351
+ * classic home screen, but newer firmware reports a real id for its
352
+ * home surfaces (`"562859"` for Roku Dynamic Menu).
353
+ */
354
+ id: string;
355
+ /**
356
+ * User-facing title (text node of `<app>`). `"Roku"` on the classic
357
+ * home screen; other home surfaces use their own name, e.g.
358
+ * `"Roku Dynamic Menu"`.
359
+ */
360
+ title: string;
361
+ /** ECP `type` attribute (`"appl"`, `"home"`, `"tvin"`, …); `""` when absent. */
362
+ type: string;
363
+ /**
364
+ * ECP `ui-location` attribute: `"home"` for the home experience, and
365
+ * the channel's own id while that channel is foregrounded. `""` when
366
+ * absent, as on firmware that predates the attribute.
367
+ */
368
+ uiLocation: string;
369
+ }
370
+
371
+ /** ECP request categories. Keys in `ECP_INTERVAL_MS` must match. */
372
+ export type EcpCategory = "query-appui" | "query-device-info" | "query-active-app" | "keypress" | "launch" | "capture";
373
+
374
+ /**
375
+ * A host that runs fixture apps in place of a physical device. The client
376
+ * deploys through it (instead of the rk server build + installer sideload)
377
+ * and then speaks ECP to the transport it returns. Lets a suite run against
378
+ * any host that answers ECP, without the specs knowing.
379
+ */
380
+ export interface DeploymentDriver {
381
+ /** Short name used where a device IP would appear (artifact names, messages). */
382
+ readonly label: string;
383
+ /** Answers ECP before the first deploy (e.g. `/query/device-info`). */
384
+ readonly initialTarget: DeviceTarget;
385
+ /** Subscribe to the console lines of whichever app is running; returns the unsubscribe. */
386
+ onLog(listener: (line: string) => void): () => void;
387
+ /** Build and start the app at `absPath`; resolves with the transport to use for it. */
388
+ deploy(request: { absPath: string; launchParams?: Record<string, string> }): Promise<DeviceTarget>;
389
+ /** Stop whatever the driver started. Idempotent. */
390
+ release?(): Promise<void>;
391
+ }
392
+
393
+ export interface RkClientOptions {
394
+ /** Run apps through this driver instead of a device at `deviceIp`. */
395
+ driver?: DeploymentDriver;
396
+ /** Device IPv4. Required directly; `createDefaultClient()` resolves from env/`.roku/`. */
397
+ deviceIp?: string;
398
+ /** Default inter-key delay for `keysequence` (ms). */
399
+ keyDelay?: number;
400
+ /** Per-request ECP timeout (ms); capture uses its own hard-coded longer timeout. */
401
+ requestTimeoutMs?: number;
402
+ /** Override per-category minimum intervals (ms). For tests only. */
403
+ intervals?: Partial<Record<EcpCategory, number>>;
404
+ /**
405
+ * Override `rk server` base URL. Precedence:
406
+ * `opts.serverUrl` → `RK_SERVER_URL` → `DEFAULT_RK_SERVER_URL`.
407
+ */
408
+ serverUrl?: string;
409
+ /** Test-only: inject a fake log session. Ignored by normal callers. */
410
+ logSession?: LogSessionLike;
411
+ }
412
+
413
+ /** Structural type for `LogSession`; kept here to avoid a types→class cycle. */
414
+ export interface LogSessionLike {
415
+ ensureStarted(): Promise<void>;
416
+ dispose(): void;
417
+ /**
418
+ * Re-attach the subscription, recovering lines the current one
419
+ * stopped delivering. Optional so an injected fake stays valid
420
+ * without it; callers feature-detect and skip the recovery.
421
+ */
422
+ resync?(): Promise<void>;
423
+ readonly buffer: LogBufferLike;
424
+ }
425
+
426
+ /** Structural type for `LogBuffer`'s public surface. */
427
+ export interface LogBufferLike {
428
+ append(line: LogLine): { seq: number; line: LogLine; ts: number };
429
+ currentPosition(): LogPosition;
430
+ linesSince(pos: LogPosition): LogLine[];
431
+ entriesSince(pos: LogPosition): Array<{ seq: number; line: LogLine; ts: number }>;
432
+ hasGapWithin(from: LogPosition, to?: LogPosition): { start: number; end: number } | null;
433
+ /**
434
+ * Return the tracked gap that contains `seq`, or `null`. Used by
435
+ * the per-match line-integrity check in `waitForLog`.
436
+ */
437
+ findGapContaining(seq: number): { start: number; end: number } | null;
438
+ subscribe(cb: (e: { seq: number; line: LogLine; ts: number }) => void): () => void;
439
+ markGapStart(): void;
440
+ markGapEnd(): void;
441
+ /** Lifetime stream-health stats. */
442
+ getStats(): {
443
+ gapCount: number;
444
+ totalGapSeqSpan: number;
445
+ lastGapAt: number | null;
446
+ openGap: boolean;
447
+ };
448
+ }
449
+
450
+ /**
451
+ * Stream-health snapshot from `getLogStreamHealth()`. `serverMetrics`
452
+ * is `null` on server fetch failure, `undefined` when the server has
453
+ * no matching session, otherwise the matched session metrics.
454
+ */
455
+ export interface LogStreamHealth {
456
+ /** Closed gaps recorded on the client buffer since construction. */
457
+ gapCount: number;
458
+ /** Sum of `(end - start)` across closed client-side gaps (seq units). */
459
+ totalGapSeqSpan: number;
460
+ /** `Date.now()` of the most recent gap close, or `null` if none. */
461
+ lastGapAt: number | null;
462
+ /** Whether the client thinks a gap is currently open (reconnecting). */
463
+ openGap: boolean;
464
+ /** Server-side metrics; see class docstring for tri-state semantics. */
465
+ serverMetrics?: {
466
+ reconnectCount: number;
467
+ totalDowntime: number;
468
+ uptime: number;
469
+ connectedAt: string | null;
470
+ lastActivity: string | null;
471
+ } | null;
472
+ }
@@ -0,0 +1,17 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * `@roku-sdk/device-test-client/support/constants`: constants a Jest config can load at parse time.
6
+ *
7
+ * The package root re-exports `RkClient`, which loads `@roku-sdk/tooling`. That package is ESM
8
+ * and uses `import.meta.url`, so a CommonJS-mode loader parsing `jest.config.ts` (ts-node, or the
9
+ * loader Jest uses) fails with `ERR_REQUIRE_ESM` before any test runs. This entry point loads only
10
+ * `src/constants.ts`, whose imports are type-only, so a config file can import from it safely.
11
+ * The same values are also exported from the package root.
12
+ *
13
+ * Only add constants a config loader needs at parse time. The file sits at this physical path so
14
+ * it also resolves under `moduleResolution: "Node"`, which ignores the `exports` map.
15
+ */
16
+
17
+ export { ARTIFACT_FILE, INTEGRATION_BUILD_OUT_DIR } from "../src/constants";
@@ -0,0 +1,27 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * `@roku-sdk/device-test-client/support/deploy`: deploy-guard helpers for test harnesses.
6
+ *
7
+ * These are for harness code that recovers from a bad deploy, not for test bodies. A harness
8
+ * that wipes an app's integration build output between deploy retries calls
9
+ * `invalidateBuildCacheForApp` with the same root, so the next `RkClient.buildAndDeploy` rebuilds
10
+ * instead of reusing a memoised result. Call the two together: wiping without invalidating
11
+ * sideloads an empty package, and invalidating without wiping rebuilds for nothing.
12
+ *
13
+ * A harness whose deployment driver sideloads through its own transport (rather than
14
+ * `RkClient.buildAndDeploy`) can reuse the build and manifest steps through `requestServerBuild`,
15
+ * `readAppTypeFromManifest` and `resolveServerUrl`.
16
+ *
17
+ * Importing this entry point loads `@roku-sdk/tooling`, which is ESM, so a CommonJS-mode config
18
+ * loader (ts-node parsing `jest.config.ts`) cannot load it. Config files should use
19
+ * `./support/constants` or `./support/paths`, which are safe to load at parse time.
20
+ *
21
+ * The file sits at this physical path, not just in the `exports` map, so it also resolves under
22
+ * `moduleResolution: "Node"`, which ignores `exports`.
23
+ */
24
+
25
+ export { readAppTypeFromManifest, requestServerBuild, resolveAppPath } from "../src/client/deploy";
26
+ export { resolveServerUrl } from "../src/client/server-url";
27
+ export { invalidateBuildCacheForApp } from "../src/client/build-cache";
@@ -0,0 +1,13 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * `@roku-sdk/device-test-client/support/paths`: where a test run writes its artifacts.
6
+ *
7
+ * Multi-device runners and Jest global setup use these to agree on the `test-results/`
8
+ * directory. Like `./support/constants`, this entry point imports only Node built-ins, so a
9
+ * CommonJS-mode config loader can import it at parse time. It must stay free of `RkClient` and
10
+ * `@roku-sdk/tooling`.
11
+ */
12
+
13
+ export { TEST_RESULTS_DIR_ENV_VAR, TEST_RESULTS_DIR_NAME, defaultTestResultsDir } from "../src/paths";