@roku-sdk/device-test-client 0.0.0-stage → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -2
- package/docs/baselines.md +221 -0
- package/docs/configuration.md +258 -0
- package/docs/dom-assertions.md +264 -0
- package/docs/errors.md +525 -0
- package/docs/examples/deep-link-cold-start.md +85 -0
- package/docs/examples/focus-navigation.md +81 -0
- package/docs/examples/log-driven-assertion.md +99 -0
- package/docs/examples/precondition-skips.md +150 -0
- package/docs/examples/screenshot-regression.md +103 -0
- package/docs/examples/smoke-test.md +69 -0
- package/docs/getting-started.md +195 -0
- package/docs/jest-integration.md +331 -0
- package/docs/log-assertions.md +314 -0
- package/docs/preconditions.md +317 -0
- package/docs/rkclient.md +655 -0
- package/docs/screenshot-assertions.md +316 -0
- package/internal/constants.ts +54 -0
- package/internal/deploy.ts +60 -0
- package/internal/paths.ts +15 -0
- package/package.json +80 -3
- package/src/assertions/assertions.ts +661 -0
- package/src/assertions/baseline-env-guard.ts +43 -0
- package/src/assertions/baselines.ts +178 -0
- package/src/assertions/screenshot-artifacts.ts +39 -0
- package/src/assertions/screenshot-diff.ts +447 -0
- package/src/client/build-cache.ts +292 -0
- package/src/client/client.ts +1656 -0
- package/src/client/deploy.ts +402 -0
- package/src/client/ecp-queue.ts +112 -0
- package/src/client/ecp-retry.ts +94 -0
- package/src/client/factory.ts +147 -0
- package/src/client/health.ts +214 -0
- package/src/client/parsers.ts +155 -0
- package/src/client/server-url.ts +137 -0
- package/src/constants.ts +471 -0
- package/src/device/device-class.ts +57 -0
- package/src/device/device-env-sync.ts +58 -0
- package/src/device/device-resolver-sync.ts +43 -0
- package/src/device/device-resolver.ts +141 -0
- package/src/device/lease-provisioning.ts +54 -0
- package/src/errors.ts +773 -0
- package/src/index.ts +186 -0
- package/src/internal/constants.ts +3 -0
- package/src/jest/artifacts.ts +87 -0
- package/src/jest/device-test-environment.ts +420 -0
- package/src/jest/failure-artifacts.ts +169 -0
- package/src/jest/global-setup.ts +133 -0
- package/src/jest/global-teardown.ts +132 -0
- package/src/jest/install-hooks.ts +488 -0
- package/src/jest/jest-setup.ts +53 -0
- package/src/jest/preconditions.ts +239 -0
- package/src/jest/telemetry.ts +331 -0
- package/src/logs/device-log-sink.ts +69 -0
- package/src/logs/log-buffer.ts +265 -0
- package/src/logs/log-session.ts +374 -0
- package/src/logs/log-stream-client.ts +296 -0
- package/src/paths.ts +65 -0
- package/src/types.ts +443 -0
|
@@ -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)`.
|