@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,317 @@
1
+ # Preconditions
2
+
3
+ Preconditions let a test suite say "this test only makes sense on a
4
+ Roku Ultra running firmware 14+ in `en_US`". The SDK exposes three
5
+ forms:
6
+
7
+ - `itOnDevice` / `describeOnDevice` — **registration-time** `it` /
8
+ `describe` wrappers that call `it.skip()` / `describe.skip()` on
9
+ mismatch. Preferred form.
10
+ - `requirePrecondition` — **synchronous** in-body guard. Throws
11
+ `PreconditionNotMetError` on mismatch (which Jest reports as a test
12
+ **failure**, not a skip).
13
+
14
+ > **Adoption in `device-tests/`.** The Solid-on-device suites
15
+ > (`lifecycle/`, `deeplink/`, `focus-restore/`, `signals/`,
16
+ > `transactions/`) each wrap their outer `describe` with
17
+ > `describeOnDevice(..., { preconditions: { firmwareVersion: ">=11.0" } })`
18
+ > (the `deeplink/` suite raises the floor to `">=12.0"` for the
19
+ > two-phase `setLaunchParamsUpdateHandler` flow). This makes the
20
+ > minimum-firmware contract executable — a CI runner pointed at
21
+ > older firmware reports `(skipped: requires firmwareVersion>=11.0,
22
+ > device has firmwareVersion=10.x)` in the Jest output instead of
23
+ > flaking mid-deploy. See
24
+ > `device-tests/preconditions/hdr-demo.test.ts` for the canonical
25
+ > two-call showcase (one `describeOnDevice`, one `itOnDevice`).
26
+ - [Core types](#core-types)
27
+ - [Precondition fields](#precondition-fields)
28
+ - [Device-class tiers](#device-class-tiers)
29
+ - `[itOnDevice](#itondevice)`
30
+ - `[describeOnDevice](#describeondevice)`
31
+ - `[requirePrecondition](#requireprecondition)`
32
+ - `[getDeviceClass](#getdeviceclass)`
33
+ - [Skip vs fail](#skip-vs-fail)
34
+ - [Fallback when no snapshot exists](#fallback-when-no-snapshot-exists)
35
+ - [Errors thrown](#errors-thrown)
36
+
37
+ ## Core types
38
+
39
+ ```ts
40
+ interface Precondition {
41
+ firmwareVersion?: string; // semver range
42
+ modelFamily?: string; // exact, against DeviceInfo.modelFamily
43
+ deviceClass?: DeviceClass; // exact, against getDeviceClass(modelName)
44
+ videoMode?: string; // prefix match
45
+ locale?: string; // exact
46
+ country?: string; // exact
47
+ library?: LibraryRequirement; // NOT IMPLEMENTED — throws PreconditionNotImplementedError
48
+ }
49
+
50
+ interface LibraryRequirement {
51
+ name: string;
52
+ version: string; // semver range
53
+ }
54
+
55
+ type DeviceClass = "high-end" | "mid-range" | "low-end" | "unknown";
56
+
57
+ interface OnDeviceTestOptions {
58
+ preconditions?: Precondition;
59
+ timeout?: number;
60
+ }
61
+
62
+ interface OnDeviceDescribeOptions {
63
+ preconditions?: Precondition;
64
+ }
65
+ ```
66
+
67
+ ## Precondition fields
68
+
69
+
70
+ | Field | Match strategy | Example |
71
+ | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
72
+ | `firmwareVersion` | semver range (Roku 4-part versions coerced to MAJOR.MINOR.PATCH) | `">=14.0"`, `">=12 <14"`, `"^13.0.0"` |
73
+ | `modelFamily` | exact vs `DeviceInfo.modelFamily` | `"Roku Ultra"` |
74
+ | `deviceClass` | exact vs `getDeviceClass(DeviceInfo.modelName)` | `"high-end"` |
75
+ | `videoMode` | prefix match vs `DeviceInfo.videoMode` | `"1080p"` matches `1080p` and `1080p60`; use `"1080p60"` for strict |
76
+ | `locale` | exact vs `DeviceInfo.locale` | `"en_US"` |
77
+ | `country` | exact vs `DeviceInfo.country` | `"US"` |
78
+ | `library` | **not implemented** — always throws | — |
79
+
80
+
81
+ **Evaluation order** is object-key iteration order (insertion order
82
+ for string keys). The caller controls which field produces the error
83
+ when multiple mismatch.
84
+
85
+ **Invalid semver ranges** throw
86
+ `RkClientError("PRECONDITION_INVALID_SEMVER_RANGE")` loudly — a
87
+ mangled range is a test bug, not a device mismatch.
88
+
89
+ ## Device-class tiers
90
+
91
+ `DeviceInfo.deviceClass` is Roku's **form-factor** classifier
92
+ (`"tv" | "stick" | "player"`). Tests usually want a performance tier
93
+ instead, derived by `getDeviceClass(modelName)`:
94
+
95
+
96
+ | modelName (case-insensitive, "Roku " prefix optional) | Tier |
97
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
98
+ | `Ultra`, `Reference RSG-4k` | `high-end` |
99
+ | `Express 4k`, `Express 4k+`, `Streaming Stick+`, `Streaming Stick 4k`, `Streaming Stick 4k+`, `Premiere`, `Premiere+`, `Ultra LT`, `Roku TV`, `4k Roku TV`, `Roku Ultra LT`, `Streambar Pro`, `Wireless Soundbar` | `mid-range` |
100
+ | `Express`, `Streaming Stick`, `Reference RSG-HD`, `Streambar` | `low-end` |
101
+ | anything else | `unknown` |
102
+
103
+
104
+ Unknown models fall through to `"unknown"` deliberately — a
105
+ `deviceClass: "high-end"` precondition on a brand-new model will
106
+ **skip** until the model has been classified (fail-closed). See
107
+ `[device-class.ts](../src/device/device-class.ts)` for the full
108
+ table.
109
+
110
+ ## `itOnDevice`
111
+
112
+ ```ts
113
+ function itOnDevice(
114
+ name: string,
115
+ fn: () => void | Promise<void>,
116
+ opts?: OnDeviceTestOptions
117
+ ): void;
118
+ ```
119
+
120
+ Registration-time wrapper. If the cached `DeviceInfo` snapshot
121
+ (`test-results/device-env.json`, written by `createGlobalSetup`)
122
+ fails the precondition, the test is registered as `it.skip(...)` with
123
+ a `(skipped: requires X, device has Y)` suffix so Jest reporters
124
+ render a useful one-line reason.
125
+
126
+ **Example — semver gate.**
127
+
128
+ ```ts
129
+ import { itOnDevice } from "@roku-sdk/device-test-client";
130
+
131
+ itOnDevice(
132
+ "uses HDR API (14+)",
133
+ async () => {
134
+ // ...
135
+ },
136
+ { preconditions: { firmwareVersion: ">=14.0" } }
137
+ );
138
+ ```
139
+
140
+ **Example — videoMode + locale.**
141
+
142
+ ```ts
143
+ itOnDevice(
144
+ "full-HD Spanish layout",
145
+ async () => {
146
+ // ...
147
+ },
148
+ {
149
+ preconditions: {
150
+ videoMode: "1080p",
151
+ locale: "es_MX"
152
+ },
153
+ timeout: 30_000
154
+ }
155
+ );
156
+ ```
157
+
158
+ **Example — device-class tier.**
159
+
160
+ ```ts
161
+ itOnDevice(
162
+ "runs 60fps animation only on high-end devices",
163
+ async () => {
164
+ // ...
165
+ },
166
+ { preconditions: { deviceClass: "high-end" } }
167
+ );
168
+ ```
169
+
170
+ ## `describeOnDevice`
171
+
172
+ ```ts
173
+ function describeOnDevice(
174
+ name: string,
175
+ fn: () => void,
176
+ opts?: OnDeviceDescribeOptions
177
+ ): void;
178
+ ```
179
+
180
+ Same contract as `itOnDevice`, but gates an entire `describe` block.
181
+
182
+ **Example.**
183
+
184
+ ```ts
185
+ import { describeOnDevice, itOnDevice } from "@roku-sdk/device-test-client";
186
+
187
+ describeOnDevice(
188
+ "player 4K playback",
189
+ () => {
190
+ itOnDevice("plays HEVC", async () => { /* ... */ });
191
+ itOnDevice("plays AV1", async () => { /* ... */ });
192
+ },
193
+ { preconditions: { videoMode: "2160p" } }
194
+ );
195
+ ```
196
+
197
+ When the outer gate skips, every test inside the block is reported
198
+ as skipped (standard Jest semantics for `describe.skip`).
199
+
200
+ ## `requirePrecondition`
201
+
202
+ ```ts
203
+ function requirePrecondition(client: RkClient, requirements: Precondition): void;
204
+ ```
205
+
206
+ **Synchronous.** Reads the live `RkClient.cachedDeviceInfo()`. Use it
207
+ for mid-test gates — e.g. after a locale change during the test body
208
+ you want to re-assert on the new locale.
209
+
210
+ **Example.**
211
+
212
+ ```ts
213
+ import { requirePrecondition } from "@roku-sdk/device-test-client";
214
+
215
+ test("library screen on firmware 13+", async () => {
216
+ requirePrecondition(client, { firmwareVersion: ">=13.0" });
217
+ await client.keypress("Home");
218
+ // ...
219
+ });
220
+ ```
221
+
222
+ **Important.** A thrown `PreconditionNotMetError` in the test body is
223
+ reported by Jest as a **failure**, not a skip — jest-circus does not
224
+ have a mid-test skip primitive. Prefer `itOnDevice` /
225
+ `describeOnDevice` whenever the intent is "skip on mismatch".
226
+
227
+ If the cache is empty (no `beforeAll` yet, no `createGlobalSetup`
228
+ snapshot) the call throws
229
+ `RkClientError("PRECONDITION_NO_DEVICE_INFO")` with actionable
230
+ remediation text.
231
+
232
+ ## `getDeviceClass`
233
+
234
+ ```ts
235
+ function getDeviceClass(modelName: string): DeviceClass;
236
+ ```
237
+
238
+ Pure — no I/O, safe from any phase. Occasionally useful in reporters
239
+ or for custom gating.
240
+
241
+ **Example.**
242
+
243
+ ```ts
244
+ import { getDeviceClass } from "@roku-sdk/device-test-client";
245
+
246
+ const tier = getDeviceClass("Roku Ultra"); // "high-end"
247
+ ```
248
+
249
+ ## Skip vs fail
250
+
251
+
252
+ | Mechanism | Mismatch = |
253
+ | --------------------- | -------------------------------------------------------- |
254
+ | `itOnDevice` | `it.skip(...)` — reported as **skipped** |
255
+ | `describeOnDevice` | `describe.skip(...)` — reported as **skipped** |
256
+ | `requirePrecondition` | throw `PreconditionNotMetError` — reported as **failed** |
257
+
258
+
259
+ Side-by-side example — same gate, different outcomes:
260
+
261
+ ```ts
262
+ // Skip when not Ultra — good hygiene for the full suite.
263
+ itOnDevice("Ultra-only feature",
264
+ async () => { /* ... */ },
265
+ { preconditions: { modelFamily: "Roku Ultra" } }
266
+ );
267
+
268
+ // Fail when not Ultra — use when the test itself promises an invariant.
269
+ test("Ultra-only feature (hard require)", async () => {
270
+ requirePrecondition(client, { modelFamily: "Roku Ultra" });
271
+ // ...
272
+ });
273
+ ```
274
+
275
+ ## Fallback when no snapshot exists
276
+
277
+ `itOnDevice` / `describeOnDevice` read from `test-results/device-env.json`
278
+ **synchronously** at registration time. When the file is missing (e.g.
279
+ a vanilla `jest` invocation without `globalSetup`), the wrapper:
280
+
281
+ - emits a one-time `console.warn(...)` per test file,
282
+ - registers the test **normally** (no skip).
283
+
284
+ This mirrors the "tests boot without a device connected" philosophy
285
+ of the rest of the framework — the first real ECP call fails fast
286
+ with actionable remediation.
287
+
288
+ To ensure the snapshot is always present in CI, call `createGlobalSetup`
289
+ from your `jest.config.ts` (see
290
+ [Jest integration](jest-integration.md)).
291
+
292
+ ## `library` — not implemented
293
+
294
+ The `library` field **always throws** `PreconditionNotImplementedError`.
295
+ Roku's ECP surface does not reliably report library versions, so we
296
+ fail loudly rather than silently passing.
297
+
298
+ `PreconditionNotImplementedError` extends `PreconditionNotMetError`,
299
+ so `itOnDevice` / `describeOnDevice` route it through the same skip
300
+ path — tests that gate on a library version are skipped with a
301
+ "library version checks are not implemented" message.
302
+
303
+ ## Errors thrown
304
+
305
+ - `PreconditionNotMetError` — a field mismatched. Fields: `field`,
306
+ `expected`, `actual`, `reason`.
307
+ - `PreconditionNotImplementedError` (extends
308
+ `PreconditionNotMetError`) — the `library` field is always a
309
+ not-implemented mismatch. Fields: `feature`, `field`, `expected`,
310
+ `actual`.
311
+ - `RkClientError("PRECONDITION_INVALID_SEMVER_RANGE")` — a
312
+ `firmwareVersion` range failed `semver.validRange`.
313
+ - `RkClientError("PRECONDITION_NO_DEVICE_INFO")` —
314
+ `requirePrecondition` called before the cache was populated.
315
+
316
+ Full field lists and recovery advice in
317
+ `[errors.md](errors.md)`.