@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -2
  3. package/docs/baselines.md +221 -0
  4. package/docs/configuration.md +258 -0
  5. package/docs/dom-assertions.md +264 -0
  6. package/docs/errors.md +525 -0
  7. package/docs/examples/deep-link-cold-start.md +85 -0
  8. package/docs/examples/focus-navigation.md +81 -0
  9. package/docs/examples/log-driven-assertion.md +99 -0
  10. package/docs/examples/precondition-skips.md +150 -0
  11. package/docs/examples/screenshot-regression.md +103 -0
  12. package/docs/examples/smoke-test.md +69 -0
  13. package/docs/getting-started.md +195 -0
  14. package/docs/jest-integration.md +331 -0
  15. package/docs/log-assertions.md +314 -0
  16. package/docs/preconditions.md +317 -0
  17. package/docs/rkclient.md +655 -0
  18. package/docs/screenshot-assertions.md +316 -0
  19. package/internal/constants.ts +54 -0
  20. package/internal/deploy.ts +60 -0
  21. package/internal/paths.ts +15 -0
  22. package/package.json +80 -3
  23. package/src/assertions/assertions.ts +661 -0
  24. package/src/assertions/baseline-env-guard.ts +43 -0
  25. package/src/assertions/baselines.ts +178 -0
  26. package/src/assertions/screenshot-artifacts.ts +39 -0
  27. package/src/assertions/screenshot-diff.ts +447 -0
  28. package/src/client/build-cache.ts +292 -0
  29. package/src/client/client.ts +1656 -0
  30. package/src/client/deploy.ts +402 -0
  31. package/src/client/ecp-queue.ts +112 -0
  32. package/src/client/ecp-retry.ts +94 -0
  33. package/src/client/factory.ts +147 -0
  34. package/src/client/health.ts +214 -0
  35. package/src/client/parsers.ts +155 -0
  36. package/src/client/server-url.ts +137 -0
  37. package/src/constants.ts +471 -0
  38. package/src/device/device-class.ts +57 -0
  39. package/src/device/device-env-sync.ts +58 -0
  40. package/src/device/device-resolver-sync.ts +43 -0
  41. package/src/device/device-resolver.ts +141 -0
  42. package/src/device/lease-provisioning.ts +54 -0
  43. package/src/errors.ts +773 -0
  44. package/src/index.ts +186 -0
  45. package/src/internal/constants.ts +3 -0
  46. package/src/jest/artifacts.ts +87 -0
  47. package/src/jest/device-test-environment.ts +420 -0
  48. package/src/jest/failure-artifacts.ts +169 -0
  49. package/src/jest/global-setup.ts +133 -0
  50. package/src/jest/global-teardown.ts +132 -0
  51. package/src/jest/install-hooks.ts +488 -0
  52. package/src/jest/jest-setup.ts +53 -0
  53. package/src/jest/preconditions.ts +239 -0
  54. package/src/jest/telemetry.ts +331 -0
  55. package/src/logs/device-log-sink.ts +69 -0
  56. package/src/logs/log-buffer.ts +265 -0
  57. package/src/logs/log-session.ts +374 -0
  58. package/src/logs/log-stream-client.ts +296 -0
  59. package/src/paths.ts +65 -0
  60. package/src/types.ts +443 -0
@@ -0,0 +1,264 @@
1
+ # DOM assertions
2
+
3
+ DOM assertions operate on the `AppUiNode` tree returned by
4
+ `client.queryAppUi()`. They come in two flavours: **pure tree helpers**
5
+ (`findNode`, `findAllNodes`, `findFocusedNode`, `expectFocused`,
6
+ `matchesPredicate`) that inspect a tree you already have, and
7
+ **baseline comparison** (`expectTree(...).toMatchBaseline(...)`) that
8
+ commits a canonical snapshot to disk.
9
+
10
+ - [Core types](#core-types)
11
+ - [Predicates](#predicates)
12
+ - `[findNode](#findnode)`
13
+ - `[findAllNodes](#findallnodes)`
14
+ - `[findFocusedNode](#findfocusednode)`
15
+ - `[expectFocused](#expectfocused)`
16
+ - `[matchesPredicate](#matchespredicate)`
17
+ - `[expectTree().toMatchBaseline(...)](#expecttreetomatchbaseline)`
18
+ - [id / name alias](#id--name-alias)
19
+ - [Errors thrown](#errors-thrown)
20
+
21
+ ## Core types
22
+
23
+ ```ts
24
+ interface AppUiNode {
25
+ tag: string; // e.g. "Label", "Group"
26
+ attrs: Record<string, string>; // every XML attribute, stringly-typed
27
+ bounds: { x: number; y: number; width: number; height: number };
28
+ focused: boolean; // true if this node or a descendant has focus
29
+ children: AppUiNode[]; // document order
30
+ }
31
+
32
+ interface NodePredicate {
33
+ /** Exact tag match. */
34
+ tag?: string;
35
+ /** Partial attribute match — every listed key must `===` match. */
36
+ attr?: Record<string, string>;
37
+ /** Exact focus match. */
38
+ focused?: boolean;
39
+ }
40
+ ```
41
+
42
+ An empty predicate (`{}`) matches every node — this is documented
43
+ behaviour, occasionally useful for "give me the first child of
44
+ anything".
45
+
46
+ ## Predicates
47
+
48
+ - `tag` — exact match against `node.tag`.
49
+ - `attr` — each listed key/value is compared with `===`. Keys not in
50
+ the predicate are ignored, so `{ attr: { id: "home" } }` matches
51
+ regardless of any other attributes.
52
+ - `focused` — matches whether the node (or an ancestor) is focused.
53
+ - **Predicates are cumulative.** Listing `tag`, `attr`, and `focused`
54
+ together ANDs them together.
55
+
56
+ ## `findNode`
57
+
58
+ ```ts
59
+ function findNode(tree: AppUiNode, predicate: NodePredicate): AppUiNode;
60
+ ```
61
+
62
+ Depth-first search returning the first match in document order. Throws
63
+ `NodeNotFoundError` (with the predicate and a truncated tree summary)
64
+ when nothing matches.
65
+
66
+ **Example.**
67
+
68
+ ```ts
69
+ import { findNode } from "@roku-sdk/device-test-client";
70
+
71
+ const tree = await client.queryAppUi();
72
+ const playButton = findNode(tree, { tag: "Button", attr: { id: "play" } });
73
+ expect(playButton.attrs.text).toBe("Play");
74
+ ```
75
+
76
+ ## `findAllNodes`
77
+
78
+ ```ts
79
+ function findAllNodes(tree: AppUiNode, predicate: NodePredicate): AppUiNode[];
80
+ ```
81
+
82
+ Same traversal as `findNode`, but returns every match in document
83
+ order. Zero matches is **not** an error — you get `[]`.
84
+
85
+ **Example.**
86
+
87
+ ```ts
88
+ import { findAllNodes } from "@roku-sdk/device-test-client";
89
+
90
+ const rows = findAllNodes(tree, { tag: "RowListItem" });
91
+ expect(rows).toHaveLength(10);
92
+ expect(rows[0]!.attrs.title).toBe("Featured");
93
+ ```
94
+
95
+ ## `findFocusedNode`
96
+
97
+ ```ts
98
+ function findFocusedNode(tree: AppUiNode): AppUiNode;
99
+ ```
100
+
101
+ Returns the **leaf** focused node (SceneGraph propagates
102
+ `focused="true"` up the ancestor chain — a naive DFS would hit the
103
+ root `<screen>` instead of the actual input target). Throws
104
+ `NodeNotFoundError("No focused node in tree")` if nothing is focused,
105
+ which is a legitimate transient state during transitions.
106
+
107
+ **Example.**
108
+
109
+ ```ts
110
+ import { findFocusedNode } from "@roku-sdk/device-test-client";
111
+
112
+ const focused = findFocusedNode(tree);
113
+ console.log(focused.tag, focused.attrs.id);
114
+ ```
115
+
116
+ ## `expectFocused`
117
+
118
+ ```ts
119
+ function expectFocused(tree: AppUiNode, predicate: NodePredicate): void;
120
+ ```
121
+
122
+ Shorthand for "I don't care where in the tree it is, just assert the
123
+ focused leaf matches this predicate". Throws `NodeNotFoundError`
124
+ (nothing focused) or `FocusedNodeMismatchError` (focused node does not
125
+ match).
126
+
127
+ **Example.**
128
+
129
+ ```ts
130
+ import { expectFocused } from "@roku-sdk/device-test-client";
131
+
132
+ await client.keypress("Down");
133
+ const tree = await client.queryAppUi();
134
+ expectFocused(tree, { tag: "Button", attr: { id: "settings" } });
135
+ ```
136
+
137
+ ## `matchesPredicate`
138
+
139
+ ```ts
140
+ function matchesPredicate(node: AppUiNode, predicate: NodePredicate): boolean;
141
+ ```
142
+
143
+ Pure boolean — useful when you already have the node (e.g. from
144
+ `waitForNode`) and want to re-assert against a different predicate
145
+ without re-traversing.
146
+
147
+ **Example.**
148
+
149
+ ```ts
150
+ import { matchesPredicate } from "@roku-sdk/device-test-client";
151
+
152
+ const node = await client.waitForNode({ attr: { id: "tile" } }, { timeout: 3_000 });
153
+ if (matchesPredicate(node, { focused: true })) {
154
+ await client.keypress("Select");
155
+ }
156
+ ```
157
+
158
+ ## `expectTree().toMatchBaseline()`
159
+
160
+ ```ts
161
+ function expectTree(node: AppUiNode): TreeAssertion;
162
+
163
+ interface TreeAssertion {
164
+ toMatchBaseline(baselinePath: string, opts?: TreeMatchOptions): Promise<void>;
165
+ }
166
+
167
+ interface TreeMatchOptions {
168
+ /** Attribute keys stripped recursively before serialization.
169
+ * Useful for transient values (timestamps, uuids) that would otherwise churn the baseline. */
170
+ exclude?: readonly string[];
171
+ /** Node tags serialized as leaves — the node is asserted, its children are not. */
172
+ excludeChildrenOf?: readonly string[];
173
+ }
174
+ ```
175
+
176
+ Serializes the subtree to canonical JSON (attrs sorted, `bounds` and
177
+ `focused` dropped as derived, children in DOM order), resolves
178
+ `<baselineRoot>/<hd|fhd>/<baselinePath>`, and either:
179
+
180
+ - **first run** (`UPDATE_BASELINES=1`) — creates the baseline
181
+ atomically, after the CI guard passes (see
182
+ `[baselines.md](baselines.md)`);
183
+ - **normal run** — compares against the stored baseline and throws
184
+ `TreeMismatchError` with `firstDiff` pointing at the first JSONPath-
185
+ ish location where the trees diverge;
186
+ - **baseline missing + no update flag** — throws
187
+ `BaselineMissingError` with instructions to re-run with
188
+ `UPDATE_BASELINES=1`.
189
+
190
+ **Example — minimal.**
191
+
192
+ ```ts
193
+ import { expectTree } from "@roku-sdk/device-test-client";
194
+
195
+ const tree = await client.queryAppUi();
196
+ await expectTree(tree).toMatchBaseline("home/initial.json");
197
+ ```
198
+
199
+ **Example — with exclusions.**
200
+
201
+ ```ts
202
+ await expectTree(tree).toMatchBaseline("player/playing.json", {
203
+ exclude: ["renderTracking", "lastRenderTimestamp"]
204
+ });
205
+ ```
206
+
207
+ **Example — native node with firmware-owned internals.** `Video`
208
+ renders a whole subtree of native chrome (caption label, trick-play
209
+ bar, spinner) that the app neither declares nor controls, and whose
210
+ shape differs between OS builds — Roku OS 16 inserts a `WeelandChild`
211
+ node that 15.3 does not have. Assert that the node is mounted without
212
+ pinning its internals:
213
+
214
+ ```ts
215
+ await expectTree(tree).toMatchBaseline("player/playing.json", {
216
+ excludeChildrenOf: ["Video"]
217
+ });
218
+ ```
219
+
220
+ **Example — subtree assertion.**
221
+
222
+ ```ts
223
+ import { findNode, expectTree } from "@roku-sdk/device-test-client";
224
+
225
+ const grid = findNode(tree, { attr: { id: "grid" } });
226
+ await expectTree(grid).toMatchBaseline("grid/10-items.json");
227
+ ```
228
+
229
+ **Common pitfall — running `UPDATE_BASELINES=1` in CI.** The CI guard
230
+ (`assertBaselineUpdatesAllowed`) throws as soon as a baseline write
231
+ would happen while `CI=true`. See
232
+ `[baselines.md#ci-guard](baselines.md#ci-guard)`.
233
+
234
+ ## id / name alias
235
+
236
+ Roku's `/query/app-ui` XML serialises the SceneGraph `id` field as a
237
+ `name="..."` attribute. Every predicate that reads `attr.id` falls
238
+ back to `attr.name` transparently, so you can continue to use the `id`
239
+ selector that appears in your JSX / XML source without writing
240
+ `name=` in tests:
241
+
242
+ ```ts
243
+ findNode(tree, { attr: { id: "play" } }); // matches <button name="play" />
244
+ findNode(tree, { attr: { name: "play" } }); // also matches — raw attribute still works
245
+ ```
246
+
247
+ The alias is strictly additive — matching on `name` directly continues
248
+ to work.
249
+
250
+ ## Errors thrown
251
+
252
+ - `NodeNotFoundError` — `findNode` / `findFocusedNode` / `expectFocused`
253
+ when no node matches. Fields: `predicate`, `treeSummary`.
254
+ - `FocusedNodeMismatchError` — `expectFocused` when a node IS focused
255
+ but doesn't match. Fields: `predicate`, `focusedSummary`.
256
+ - `WaitForNodeTimeoutError` — `client.waitForNode` on timeout. Fields:
257
+ `predicate`, `timeout`, `context.lastTree`.
258
+ - `BaselineMissingError` — `expectTree` against a baseline that
259
+ doesn't exist. Fields: `baselinePath`, `resolution`, `videoMode`.
260
+ - `TreeMismatchError` — `expectTree` with a diff. Fields:
261
+ `baselinePath`, `firstDiff`, `expectedSnippet`, `actualSnippet`.
262
+
263
+ Full field lists and recovery advice in
264
+ `[errors.md](errors.md)`.