@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.
- package/BREAKING_CHANGES.md +17 -0
- 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 +544 -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 +349 -0
- package/docs/preconditions.md +317 -0
- package/docs/rkclient.md +657 -0
- package/docs/screenshot-assertions.md +316 -0
- package/package.json +81 -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 +1964 -0
- package/src/client/deploy.ts +407 -0
- package/src/client/ecp-queue.ts +112 -0
- package/src/client/ecp-retry.ts +94 -0
- package/src/client/factory.ts +170 -0
- package/src/client/health.ts +345 -0
- package/src/client/parsers.ts +162 -0
- package/src/client/server-url.ts +137 -0
- package/src/constants.ts +520 -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 +774 -0
- package/src/index.ts +181 -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 +137 -0
- package/src/jest/global-teardown.ts +132 -0
- package/src/jest/install-hooks.ts +490 -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 +278 -0
- package/src/logs/log-session.ts +436 -0
- package/src/logs/log-stream-client.ts +296 -0
- package/src/paths.ts +65 -0
- package/src/types.ts +472 -0
- package/support/constants.ts +17 -0
- package/support/deploy.ts +27 -0
- 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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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";
|