@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,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)`.
|