@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,99 @@
1
+ # Example: log-driven assertion
2
+
3
+ End-to-end causality pattern: snapshot a log position, trigger a
4
+ device action, then `waitForLog` for the app-level ack. This is the
5
+ canonical way to observe behaviour that *isn't* reflected in the DOM
6
+ (analytics, network fires, side effects).
7
+
8
+ ## Prerequisites
9
+
10
+ - App emits a deterministic log line when the observed action
11
+ completes (e.g.
12
+ `analytics:track event=tile_click id=tile-7`).
13
+ - Log streaming has been installed — `installDeviceTestHooks` wires
14
+ `ensureLogSession` automatically in `beforeEach`.
15
+
16
+ ## Test file
17
+
18
+ ```ts
19
+ // tests/analytics.test.ts
20
+ import {
21
+ installDeviceTestHooks
22
+ } from "@roku-sdk/device-test-client";
23
+
24
+ const ctx = installDeviceTestHooks();
25
+
26
+ describe("tile click analytics", () => {
27
+ test("fires track event with correct id", async () => {
28
+ const client = ctx.getClient();
29
+
30
+ await client.buildAndDeploy("apps/grid", {
31
+ readyNode: { attr: { id: "grid" } }
32
+ });
33
+
34
+ // ─── Causality: snapshot → action → wait ───────────────────
35
+ const pos = client.logPosition();
36
+ await client.keypress("Select");
37
+
38
+ const line = await client.waitForLog(
39
+ /analytics:track event=(\w+) id=(\S+)/,
40
+ { since: pos, timeout: 3_000 }
41
+ );
42
+
43
+ const [, event, id] = line.message.match(
44
+ /analytics:track event=(\w+) id=(\S+)/
45
+ )!;
46
+ expect(event).toBe("tile_click");
47
+ expect(id).toMatch(/^tile-\d+$/);
48
+
49
+ // ─── Negative assertion: no double-fire within 500 ms ──────
50
+ await client.expectNoLog(/analytics:track event=tile_click/, {
51
+ since: pos,
52
+ window: 500,
53
+ // `pos` is before the keypress, so this tests "no OTHER
54
+ // tile_click event landed beyond the one we just matched".
55
+ // For strict no-double-fire, snapshot `pos2 = logPosition()`
56
+ // AFTER the first match and use that instead.
57
+ });
58
+ });
59
+ });
60
+ ```
61
+
62
+ ## The causality pattern
63
+
64
+ ```text
65
+ T=0 client.logPosition() ← snapshot
66
+ T=0+ε client.keypress("Select") ← trigger
67
+ T=0+Δ device emits log line ← observation
68
+ T=Δ+n waitForLog matches ← assertion
69
+ ```
70
+
71
+ The `since: pos` argument is what makes this reliable. Without it,
72
+ `waitForLog` scans the buffer's minimum retained seq — which on a
73
+ long-lived worker could match a line from a previous test.
74
+
75
+ ### Negative assertions
76
+
77
+ `expectNoLog(pattern, { since, window })` runs the opposite: it
78
+ throws `UnexpectedLogError` if *any* line in the window matches.
79
+ Use it for "didn't accidentally navigate away", "didn't log an
80
+ error", "didn't double-fire".
81
+
82
+ `window` is optional — without it, `expectNoLog` evaluates
83
+ immediately against already-buffered lines and returns.
84
+
85
+ ## Common pitfalls
86
+
87
+ - **Don't snapshot AFTER the action.** The observed log line may
88
+ arrive before the snapshot; subsequent `waitForLog` never
89
+ matches.
90
+ - **Use `{ since }`, not `{ timeout }` alone.** `timeout` is a
91
+ watchdog; `since` is what scopes the window. Both are required
92
+ for deterministic tests.
93
+ - **Handle `LogStreamGapError`.** If the SSE stream dropped and
94
+ reconnected during the window, the missing lines could have
95
+ matched. Decide per-test whether to retry or surface.
96
+ - **Don't leak buffer state between tests.** `installDeviceTestHooks`
97
+ calls `disposeLogSession` in `afterEach` so each test starts with
98
+ a fresh position. Custom setups must mirror that.
99
+
@@ -0,0 +1,150 @@
1
+ # Example: precondition skips
2
+
3
+ Mix `itOnDevice` and `describeOnDevice` to gate tests on firmware,
4
+ device class, locale, and video mode. Demonstrates the difference
5
+ between "skip with reason" (registration-time wrappers) and "fail
6
+ with reason" (runtime `requirePrecondition`).
7
+
8
+ ## Prerequisites
9
+
10
+ - `createGlobalSetup` has run and written `test-results/device-env.json`
11
+ — preconditions read the cached `DeviceInfo` synchronously at
12
+ registration time.
13
+
14
+ ## Test file
15
+
16
+ ```ts
17
+ // tests/precondition-matrix.test.ts
18
+ import {
19
+ describeOnDevice,
20
+ itOnDevice,
21
+ requirePrecondition,
22
+ installDeviceTestHooks
23
+ } from "@roku-sdk/device-test-client";
24
+
25
+ const ctx = installDeviceTestHooks();
26
+
27
+ // Whole suite gated on device class + video mode.
28
+ describeOnDevice(
29
+ "4K playback",
30
+ () => {
31
+ itOnDevice(
32
+ "plays HEVC content",
33
+ async () => {
34
+ const client = ctx.getClient();
35
+ await client.buildAndDeploy("apps/player");
36
+ // ...
37
+ }
38
+ );
39
+
40
+ itOnDevice(
41
+ "uses HDR API (firmware 14+)",
42
+ async () => {
43
+ const client = ctx.getClient();
44
+ // ...
45
+ },
46
+ {
47
+ preconditions: { firmwareVersion: ">=14.0" }
48
+ }
49
+ );
50
+ },
51
+ {
52
+ preconditions: {
53
+ deviceClass: "high-end",
54
+ videoMode: "2160p"
55
+ }
56
+ }
57
+ );
58
+
59
+ describe("locale-specific UI", () => {
60
+ it("renders Spanish strings", async () => {
61
+ // Runtime form — fails the test if the device is not es-ES.
62
+ requirePrecondition({ locale: "es-ES" });
63
+
64
+ const client = ctx.getClient();
65
+ await client.buildAndDeploy("apps/i18n");
66
+ // ...
67
+ });
68
+ });
69
+ ```
70
+
71
+ ## Walkthrough
72
+
73
+ ### `describeOnDevice` — whole-suite gate
74
+
75
+ ```ts
76
+ describeOnDevice("suite name", body, { preconditions: {…} });
77
+ ```
78
+
79
+ If the device doesn't satisfy every precondition, the suite
80
+ registers as `describe.skip(...)` with the reason interpolated into
81
+ the title. **All** `itOnDevice` and plain `it` inside the block
82
+ skip with it.
83
+
84
+ ### `itOnDevice` — per-test gate
85
+
86
+ ```ts
87
+ itOnDevice("test name", body, { preconditions: {…} });
88
+ ```
89
+
90
+ Evaluates preconditions at registration time. If the device lacks
91
+ the required fields, the test registers as `it.skip(...)`. If the
92
+ device DOES satisfy them, the test runs and its callback executes
93
+ with the usual `installDeviceTestHooks` plumbing.
94
+
95
+ Inside a `describeOnDevice` block, `itOnDevice`'s preconditions
96
+ **merge with** the enclosing block's — both must pass for the
97
+ test to run.
98
+
99
+ ### `requirePrecondition` — runtime gate
100
+
101
+ ```ts
102
+ it("something", async () => {
103
+ requirePrecondition({ locale: "es-ES" });
104
+ // ...
105
+ });
106
+ ```
107
+
108
+ Throws `PreconditionNotMetError` inside the test body. Jest reports
109
+ the test as *failed*, not skipped. Use this only when the check
110
+ genuinely needs to be runtime (e.g. depends on another test's side
111
+ effect).
112
+
113
+ ## Precondition matching rules
114
+
115
+ | Field | Matcher | Example |
116
+ |-------|---------|---------|
117
+ | `firmwareVersion` | semver range | `">=12.0"`, `"14.x"`, `"<15.0"` |
118
+ | `modelFamily` | exact string | `"Roku Ultra"` |
119
+ | `deviceClass` | derived tier | `"high-end"` \| `"mid-range"` \| `"low-end"` |
120
+ | `videoMode` | prefix | `"2160p"` matches `"2160p60"`, `"2160p"` |
121
+ | `locale` | exact string | `"en-US"` |
122
+ | `country` | exact string | `"US"` |
123
+ | `library` | **not implemented** — throws `PreconditionNotImplementedError` |
124
+
125
+ `deviceClass` is derived from `modelName` via
126
+ [`getDeviceClass`](../preconditions.md#devicecomputing-tiers).
127
+ See the full tier table there.
128
+
129
+ ## Skip vs fail cheat sheet
130
+
131
+ | Wrapper | Missing device info | Field mismatch | Not-implemented field |
132
+ |---------|---------------------|----------------|-----------------------|
133
+ | `itOnDevice` | skip + "missing device info" reason | skip + reason | skip + "not implemented" reason |
134
+ | `describeOnDevice` | skip whole block | skip whole block | skip whole block |
135
+ | `requirePrecondition` | throws `PreconditionNotMetError({reason:"missing-device-info"})` | throws `PreconditionNotMetError({reason:"mismatch"})` | throws `PreconditionNotImplementedError` |
136
+
137
+ ## Common pitfalls
138
+
139
+ - **`device-env.json` must exist at registration time.** If a worker
140
+ starts before `globalSetup` writes the file, preconditions
141
+ degrade gracefully to "skip" with the `missing-device-info`
142
+ reason — tests don't run but no failures either. Hooks-based
143
+ setups never hit this.
144
+ - **`library` is not yet implemented.** Any precondition
145
+ referencing it throws `PreconditionNotImplementedError`, which
146
+ the registration-time wrappers turn into `it.skip(...)` so the
147
+ test doesn't regress to red.
148
+ - **Prefer `itOnDevice` over `requirePrecondition`.** Skip is the
149
+ correct CI signal for "device can't run this test"; fail is
150
+ reserved for "device should have been able but wasn't".
@@ -0,0 +1,103 @@
1
+ # Example: screenshot regression
2
+
3
+ Stable-capture + baseline-diff flow with a mask for the live clock
4
+ widget. Demonstrates how to gate screenshot assertions on frame
5
+ stability and how to regenerate baselines without accidentally
6
+ green-lighting real regressions.
7
+
8
+ ## Prerequisites
9
+
10
+ - Baseline PNG committed at
11
+ `baselines/hd/home/initial.png` (or `fhd/` if your device runs
12
+ 1080p). See [baselines.md](../baselines.md) for the layout rules.
13
+ - `setBaselineRoot(...)` called during Jest's `setupFilesAfterEnv`.
14
+
15
+ ## Test file
16
+
17
+ ```ts
18
+ // tests/screenshot-home.test.ts
19
+ import {
20
+ installDeviceTestHooks,
21
+ expectScreenshot,
22
+ type MaskRegion
23
+ } from "@roku-sdk/device-test-client";
24
+
25
+ const ctx = installDeviceTestHooks();
26
+
27
+ const CLOCK_MASK: MaskRegion = {
28
+ // top-right HH:MM, coords in "hd" space
29
+ x: 1120,
30
+ y: 20,
31
+ width: 140,
32
+ height: 40
33
+ };
34
+
35
+ describe("home screen regression", () => {
36
+ test("matches baseline on boot", async () => {
37
+ const client = ctx.getClient();
38
+
39
+ await client.buildAndDeploy("apps/home", {
40
+ readyNode: { attr: { id: "home-screen" } }
41
+ });
42
+
43
+ const png = await client.captureStableScreenshot({
44
+ timeout: 8_000,
45
+ interval: 500,
46
+ mask: [CLOCK_MASK]
47
+ });
48
+
49
+ await expectScreenshot(png).toMatchBaseline("home/initial.png", {
50
+ threshold: 0.005,
51
+ mask: [CLOCK_MASK]
52
+ });
53
+ });
54
+ });
55
+ ```
56
+
57
+ ## Walkthrough
58
+
59
+ 1. `captureStableScreenshot` loops `captureScreenshot` until two
60
+ consecutive PNGs are pixel-identical outside the mask, or
61
+ `timeout` elapses (then throws `ScreenshotNotStableError`). The
62
+ mask is applied inside the stability check too — otherwise the
63
+ ticking clock would keep the loop running forever.
64
+ 2. `expectScreenshot(png).toMatchBaseline(path, options)`:
65
+ - Resolves `path` against `baselineRoot` + the active client's
66
+ resolution subdir (`hd/` or `fhd/`).
67
+ - On missing baseline + `UPDATE_BASELINES=1`: writes the PNG
68
+ atomically and returns. Without the env var: throws
69
+ `ScreenshotBaselineMissingError`.
70
+ - On dimension mismatch: throws
71
+ `ScreenshotDimensionsMismatchError` (no diff written).
72
+ - On pixel mismatch over `threshold`: writes
73
+ `test-results/diffs/<flattened>.diff.png` and throws
74
+ `ScreenshotMismatchError` with `context.diffPath`.
75
+ 3. `threshold: 0.005` = up to 0.5 % of unmasked pixels may differ
76
+ before the assertion fails. Tune per scene — splash screens can
77
+ be near-zero; menu screens with subtle gradients may need 0.01
78
+ or more.
79
+
80
+ ## Regenerating the baseline
81
+
82
+ When an intentional UI change makes the old baseline wrong:
83
+
84
+ ```bash
85
+ UPDATE_BASELINES=1 pnpm jest tests/screenshot-home.test.ts
86
+ ```
87
+
88
+ The env var is **blocked in CI** (both `CI` and `UPDATE_BASELINES`
89
+ truthy simultaneously raises an error). Commit the regenerated PNG
90
+ only after a visual review.
91
+
92
+ ## Common pitfalls
93
+
94
+ - **Masks are in baseline-canvas coords, not device coords.** The
95
+ mask is applied before diffing, so the region must align with the
96
+ baseline image pixels. If the baseline is HD and the device is
97
+ FHD, capture a fresh FHD baseline — do not scale masks by hand.
98
+ - **Don't raise the threshold to mute a real regression.** If the
99
+ diff PNG shows a genuine change, commit a new baseline instead of
100
+ loosening the threshold.
101
+ - **Avoid capturing mid-transition.** Without
102
+ `captureStableScreenshot`, a single-frame capture will flake.
103
+ - **Never commit `test-results/diffs/`.** Add it to `.gitignore`.
@@ -0,0 +1,69 @@
1
+ # Example: smoke test
2
+
3
+ The shortest possible end-to-end test: deploy the app, confirm the
4
+ launch screen rendered, send a keypress, confirm a DOM transition.
5
+ Use this as a template for the first test in a fresh repo.
6
+
7
+ ## Prerequisites
8
+
9
+ - Jest config wired per [getting-started.md](../getting-started.md).
10
+ - A Roku device reachable on the LAN with `rk server` running.
11
+ - A sideloadable app at `apps/smoke` (or substitute your own path).
12
+
13
+ ## Test file
14
+
15
+ ```ts
16
+ // tests/smoke.test.ts
17
+ import {
18
+ findNode,
19
+ installDeviceTestHooks
20
+ } from "@roku-sdk/device-test-client";
21
+
22
+ const ctx = installDeviceTestHooks();
23
+
24
+ describe("smoke", () => {
25
+ test("launches and responds to input", async () => {
26
+ const client = ctx.getClient();
27
+
28
+ await client.buildAndDeploy("apps/smoke", {
29
+ readyNode: { attr: { id: "home-screen" } }
30
+ });
31
+
32
+ const tree = await client.queryAppUi();
33
+ const playButton = findNode(tree, {
34
+ tag: "Button",
35
+ attr: { id: "play" }
36
+ });
37
+ expect(playButton.attrs.text).toBe("Play");
38
+
39
+ await client.keypress("Select");
40
+
41
+ await client.waitForNode(
42
+ { attr: { id: "player-screen" } },
43
+ { timeout: 5_000 }
44
+ );
45
+ });
46
+ });
47
+ ```
48
+
49
+ ## What the test exercises
50
+
51
+ 1. `installDeviceTestHooks()` wires `beforeAll` / `beforeEach` /
52
+ `afterEach` hooks that resolve the device IP, build the
53
+ `RkClient`, reset the log session per test, and collect failure
54
+ artifacts on uncaught errors.
55
+ 2. `buildAndDeploy` sideloads the app and waits until `readyNode`
56
+ appears in the SceneGraph tree. Any crash or post-deploy system
57
+ dialog during readiness polling raises `DeployFailedError` /
58
+ `AppCrashedError`, which `installDeviceTestHooks` turns into a
59
+ full failure bundle (screenshot + dom + logs).
60
+ 3. `queryAppUi` fetches the current tree; `findNode` throws
61
+ `NodeNotFoundError` if the predicate doesn't match.
62
+ 4. `keypress("Select")` is ECP; subsequent `waitForNode` polls
63
+ `queryAppUi` on the default 200 ms cadence.
64
+
65
+ ## Why this is a template, not a framework
66
+
67
+ No retries, no conditional logic, no preconditions — just four
68
+ actions and two assertions. Add preconditions, screenshot checks,
69
+ or log gates by layering them in. The other examples show how.
@@ -0,0 +1,195 @@
1
+ # Getting started
2
+
3
+ This page walks you from an empty workspace to a passing integration test
4
+ running against a real Roku device. If you already have a Roku sideload
5
+ toolchain (via `rk`) you can skip straight to
6
+ [Step 3 — author your first test](#step-3--author-your-first-test).
7
+
8
+ ## Prerequisites
9
+
10
+ - **Node.js 24+** — the SDK-wide supported runtime. The package needs a
11
+ global `fetch`; runtimes without one fail log-session construction with
12
+ "fetch implementation required".
13
+ - **Jest 29 or 30** as the test runner.
14
+ - A Roku device on your local network with **developer mode enabled** and
15
+ a known **developer password**.
16
+ - The `**rk` CLI** (from the `[@roku-sdk/tooling](../../tooling)` package)
17
+ configured via `rk device add <ip>` so `.roku/roku-dev-config.json`
18
+ exists next to your tests **OR** the env-var pair
19
+ `RK_DEVICE_IP` + `RK_DEVICE_PASSWORD` set (intended for CI — see
20
+ [Configuration → Device credentials](configuration.md#device-credentials)).
21
+ When both env vars are set the `.roku/` walk is skipped entirely. With
22
+ only `RK_DEVICE_IP` set, the password is taken from the `.roku/` entry
23
+ with that IP.
24
+ - `**rk server` running** on the host if you plan to use log assertions.
25
+ The daemon tails the Roku telnet log port (8085) and exposes an HTTP /
26
+ SSE front door that the SDK consumes.
27
+
28
+ Verify the device is reachable:
29
+
30
+ ```bash
31
+ rk device list # the active device should have a green dot
32
+ rk device ping # smoke-checks /query/device-info
33
+ ```
34
+
35
+ ## Step 1 — install
36
+
37
+ ```bash
38
+ pnpm add -D @roku-sdk/device-test-client jest ts-jest @types/jest
39
+ # or npm/yarn equivalents
40
+ ```
41
+
42
+ ## Step 2 — wire Jest
43
+
44
+ Drop these three files next to your tests. The exact paths are examples;
45
+ adjust them in `jest.config.ts` if you prefer a different layout.
46
+
47
+ ### `jest.config.ts`
48
+
49
+ ```ts
50
+ import type { Config } from "jest";
51
+
52
+ const config: Config = {
53
+ preset: "ts-jest",
54
+ testEnvironment: "@roku-sdk/device-test-client/environment",
55
+ testMatch: ["<rootDir>/tests/**/*.test.ts"],
56
+ globalSetup: "<rootDir>/tests/support/global-setup.ts",
57
+ globalTeardown: "<rootDir>/tests/support/global-teardown.ts",
58
+ setupFilesAfterEach: ["<rootDir>/tests/support/setup.ts"],
59
+ maxWorkers: 1,
60
+ testTimeout: 60_000
61
+ };
62
+
63
+ export default config;
64
+ ```
65
+
66
+ The custom `testEnvironment` is what captures per-failure bundles (see
67
+ [Jest integration](jest-integration.md)). `maxWorkers: 1` is required —
68
+ tests share a single device.
69
+
70
+ ### `tests/support/global-setup.ts`
71
+
72
+ ```ts
73
+ import { createGlobalSetup } from "@roku-sdk/device-test-client";
74
+ import { resolve } from "node:path";
75
+
76
+ export default createGlobalSetup({
77
+ testResultsDir: resolve(__dirname, "../../test-results")
78
+ });
79
+ ```
80
+
81
+ `createGlobalSetup` resolves the active device, queries `DeviceInfo`,
82
+ writes `test-results/device-env.json`, and boots a fresh `rk server` log
83
+ session. See
84
+ `[createGlobalSetup](jest-integration.md#creategloballsetup)` for the full
85
+ contract.
86
+
87
+ ### `tests/support/global-teardown.ts`
88
+
89
+ ```ts
90
+ import { createGlobalTeardown } from "@roku-sdk/device-test-client";
91
+
92
+ export default createGlobalTeardown({ exitToHome: true });
93
+ ```
94
+
95
+ ### `tests/support/setup.ts`
96
+
97
+ ```ts
98
+ import {
99
+ createDefaultClient,
100
+ installDeviceTestHooks
101
+ } from "@roku-sdk/device-test-client";
102
+ import { resolve } from "node:path";
103
+
104
+ const client = createDefaultClient();
105
+
106
+ installDeviceTestHooks({
107
+ client,
108
+ baselineRoot: resolve(__dirname, "../baselines"),
109
+ testResultsDir: resolve(__dirname, "../../test-results"),
110
+ diffRoot: resolve(__dirname, "../../test-results/diffs"),
111
+ deviceEnvFile: resolve(
112
+ __dirname,
113
+ "../../test-results/device-env.json"
114
+ )
115
+ });
116
+
117
+ export { client };
118
+ ```
119
+
120
+ `installDeviceTestHooks` is the **single** wiring entry point in v2.0.0.
121
+ It:
122
+
123
+ - registers the `client` so `expectTree` / `expectScreenshot` /
124
+ `itOnDevice` / `describeOnDevice` see the active device without extra
125
+ setup calls,
126
+ - anchors the **baselines root** (used by `expectTree` /
127
+ `expectScreenshot`),
128
+ - anchors the **screenshot diff root** (PNGs for failed
129
+ `expectScreenshot` runs),
130
+ - anchors the **device-env snapshot path** that
131
+ `readCachedDeviceInfoSync` (via registration-time preconditions)
132
+ reads,
133
+ - starts the log SSE session in `beforeAll`,
134
+ - aborts every subsequent test in a worker if the device flagged itself
135
+ unhealthy (fail-fast cascade),
136
+ - flushes per-failure bundles in the custom test environment.
137
+
138
+ > **Migrated from v1.x?** The separate `setActiveClient(...)` /
139
+ > `setBaselineRoot(...)` / `setScreenshotDiffRoot(...)` /
140
+ > `setDeviceEnvFile(...)` calls you may have in an older `setup.ts`
141
+ > were removed from the barrel in v2.0.0 — pass the same values as
142
+ > `installDeviceTestHooks` options above. See the `installDeviceTestHooks` options above for the supported setup,
143
+ > and [Versioning](../README.md#versioning) for the public API contract.
144
+
145
+ ## Step 3 — author your first test
146
+
147
+ ```ts
148
+ // tests/smoke.test.ts
149
+ import { client } from "./support/setup";
150
+
151
+ test("app boots to the home scene", async () => {
152
+ await client.buildAndDeploy("apps/buttons", {
153
+ readyNode: { attr: { id: "home-title" } }
154
+ });
155
+
156
+ await client.keypress("Select");
157
+
158
+ const firstButton = await client.waitForNode(
159
+ { focused: true, attr: { id: "first-button" } },
160
+ { timeout: 3_000 }
161
+ );
162
+
163
+ expect(firstButton.attrs.text).toBe("Play");
164
+ });
165
+ ```
166
+
167
+ Run the suite:
168
+
169
+ ```bash
170
+ pnpm jest
171
+ ```
172
+
173
+ On success you should see:
174
+
175
+ ```
176
+ [device-test-client] device: Roku Ultra (4800X), firmware 14.1.0,
177
+ videoMode 1080p60, locale en_US
178
+ PASS tests/smoke.test.ts
179
+ ```
180
+
181
+ On failure, look under `test-results/failures/<sanitized-test-name>/` for
182
+ the screenshot, DOM tree, recent logs, and error bundle — the reviewer
183
+ gets everything they need to diagnose without re-running.
184
+
185
+ ## Where to next?
186
+
187
+ - `[RkClient` reference](rkclient.md) — every method, every option.
188
+ - [DOM assertions](dom-assertions.md) — `findNode`, `waitForNode`,
189
+ `expectTree`.
190
+ - [Screenshot assertions](screenshot-assertions.md) — `expectScreenshot`
191
+ with masks and thresholds.
192
+ - [Preconditions](preconditions.md) — skip tests cleanly on unsupported
193
+ hardware.
194
+ - [Examples](examples/) — end-to-end walkthroughs.
195
+