@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,316 @@
|
|
|
1
|
+
# Screenshot assertions
|
|
2
|
+
|
|
3
|
+
Screenshot assertions commit a rendered frame to disk as the ground
|
|
4
|
+
truth for a UI state and verify future runs stay within a configurable
|
|
5
|
+
mismatch budget. They share the baseline layout, resolution routing,
|
|
6
|
+
and `UPDATE_BASELINES=1` workflow with
|
|
7
|
+
[DOM assertions](dom-assertions.md), but the comparison engine is
|
|
8
|
+
[pixelmatch](https://github.com/mapbox/pixelmatch) and failures write
|
|
9
|
+
an annotated diff PNG to `test-results/diffs/`.
|
|
10
|
+
|
|
11
|
+
- [Core types](#core-types)
|
|
12
|
+
- [Stable capture](#stable-capture)
|
|
13
|
+
- `[expectScreenshot().toMatchBaseline(...)](#expectscreenshottomatchbaseline)`
|
|
14
|
+
- [Thresholds — aggregate vs per-pixel](#thresholds--aggregate-vs-per-pixel)
|
|
15
|
+
- [Masks](#masks)
|
|
16
|
+
- [Alignment tolerance](#alignment-tolerance)
|
|
17
|
+
- [Diff PNGs](#diff-pngs)
|
|
18
|
+
- [Diff-root override](#diff-root-override)
|
|
19
|
+
- `[UPDATE_BASELINES=1` refresh](#update_baselines1-refresh)
|
|
20
|
+
- [Errors thrown](#errors-thrown)
|
|
21
|
+
|
|
22
|
+
## Core types
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
interface ScreenshotMatchOptions {
|
|
26
|
+
/** Mismatch budget as a fraction of UNMASKED pixels. Default 0.01 (1%). */
|
|
27
|
+
threshold?: number;
|
|
28
|
+
/** Regions zeroed out in BOTH images before comparison. */
|
|
29
|
+
mask?: MaskRegion[];
|
|
30
|
+
/** Whole-pixel slack for device-dependent re-centering. Default none. */
|
|
31
|
+
alignTolerance?: AlignTolerance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface MaskRegion {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface AlignTolerance {
|
|
42
|
+
/** Max horizontal translation, 0..ALIGN_TOLERANCE_MAX_PX. Default 0. */
|
|
43
|
+
x?: number;
|
|
44
|
+
/** Max vertical translation, 0..ALIGN_TOLERANCE_MAX_PX. Default 0. */
|
|
45
|
+
y?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface CaptureStableOptions {
|
|
49
|
+
/** Overall wait budget (ms). */
|
|
50
|
+
timeout: number;
|
|
51
|
+
/** Minimum gap between captures (ms). Default 500. */
|
|
52
|
+
interval?: number;
|
|
53
|
+
/** Regions ignored when deciding "two frames are identical". */
|
|
54
|
+
mask?: MaskRegion[];
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Stable capture
|
|
59
|
+
|
|
60
|
+
Raw `client.captureScreenshot()` returns whatever is on screen at that
|
|
61
|
+
instant — typically a mid-animation frame. For baseline comparison you
|
|
62
|
+
want a **settled** frame: that's `captureStableScreenshot`.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
captureStableScreenshot(opts: CaptureStableOptions): Promise<Buffer>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Polls until two successive captures are pixel-identical outside `mask`
|
|
69
|
+
regions and returns the newer PNG. `assertAppAlive` runs before each
|
|
70
|
+
capture so a crash during polling surfaces as `AppCrashedError`
|
|
71
|
+
instead of a timeout.
|
|
72
|
+
|
|
73
|
+
**Example.**
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const png = await client.captureStableScreenshot({
|
|
77
|
+
timeout: 5_000,
|
|
78
|
+
mask: [{ x: 0, y: 0, width: 1920, height: 80 }] // hide the clock/status bar
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Throws `ScreenshotNotStableError` with `iterations`, `lastDiffCount`,
|
|
83
|
+
`firstDiffPoint`, `maskedRegions`, and `lastPng` on timeout.
|
|
84
|
+
|
|
85
|
+
## `expectScreenshot().toMatchBaseline()`
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
function expectScreenshot(png: Buffer): ScreenshotAssertion;
|
|
89
|
+
|
|
90
|
+
interface ScreenshotAssertion {
|
|
91
|
+
toMatchBaseline(baselinePath: string, opts?: ScreenshotMatchOptions): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Resolves `baselinePath` against the active baseline root + resolution
|
|
96
|
+
subdir (`hd/` or `fhd/`), reads the stored PNG, applies masks, runs
|
|
97
|
+
pixelmatch, then either:
|
|
98
|
+
|
|
99
|
+
- **equal within threshold** → assertion passes;
|
|
100
|
+
- **missing baseline + `UPDATE_BASELINES=1`** → atomically writes the
|
|
101
|
+
actual PNG at the baseline path and passes;
|
|
102
|
+
- **missing baseline, normal run** → throws
|
|
103
|
+
`ScreenshotBaselineMissingError`;
|
|
104
|
+
- **dimensions differ** → throws `ScreenshotDimensionsMismatchError`
|
|
105
|
+
(no diff PNG — pixelmatch cannot compare at different sizes);
|
|
106
|
+
- **mismatch above threshold** → writes a diff PNG and throws
|
|
107
|
+
`ScreenshotMismatchError`.
|
|
108
|
+
|
|
109
|
+
**Minimal example.**
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { expectScreenshot } from "@roku-sdk/device-test-client";
|
|
113
|
+
|
|
114
|
+
const png = await client.captureStableScreenshot({ timeout: 5_000 });
|
|
115
|
+
await expectScreenshot(png).toMatchBaseline("home/initial.png");
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Realistic example — mask, explicit threshold.**
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const png = await client.captureStableScreenshot({
|
|
122
|
+
timeout: 5_000,
|
|
123
|
+
mask: [{ x: 0, y: 0, width: 1920, height: 80 }]
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await expectScreenshot(png).toMatchBaseline("home/initial.png", {
|
|
127
|
+
threshold: 0.005, // 0.5% of unmasked pixels may differ
|
|
128
|
+
mask: [{ x: 0, y: 0, width: 1920, height: 80 }]
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Note that `mask` is applied in **both** the capture step and the
|
|
133
|
+
assertion. The capture mask prevents animating content from ever
|
|
134
|
+
being considered "stable"; the assertion mask makes sure the same
|
|
135
|
+
region is ignored during comparison.
|
|
136
|
+
|
|
137
|
+
## Thresholds — aggregate vs per-pixel
|
|
138
|
+
|
|
139
|
+
The SDK decouples two thresholds on purpose:
|
|
140
|
+
|
|
141
|
+
- **Aggregate ratio** (`opts.threshold`, default `0.01`) — the
|
|
142
|
+
fraction of unmasked pixels that may differ for the assertion to
|
|
143
|
+
pass.
|
|
144
|
+
- **Per-pixel YIQ color distance** — fixed internal constant
|
|
145
|
+
(`PIXELMATCH_COLOR_THRESHOLD = 0.1`), not exposed. Tuning this
|
|
146
|
+
would quietly shift what counts as a "different" pixel, which is
|
|
147
|
+
not a knob most tests actually want.
|
|
148
|
+
|
|
149
|
+
So `{ threshold: 0.5 }` means "up to 50% of pixels may differ before
|
|
150
|
+
I fail" — it does **not** mean "also loosen per-pixel sensitivity."
|
|
151
|
+
|
|
152
|
+
## Masks
|
|
153
|
+
|
|
154
|
+
Masks are validated and clamped against the image canvas:
|
|
155
|
+
|
|
156
|
+
- Zero-width / zero-height → `ScreenshotMaskError`.
|
|
157
|
+
- Fully out-of-bounds → `ScreenshotMaskError`.
|
|
158
|
+
- Partially out-of-bounds → silently clamped to canvas.
|
|
159
|
+
|
|
160
|
+
The SDK zeroes masked pixels in **both** images before invoking
|
|
161
|
+
pixelmatch and excludes them from the ratio denominator, so a large
|
|
162
|
+
mask does not inflate the effective threshold.
|
|
163
|
+
|
|
164
|
+
**Example — masking the player clock.**
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
await expectScreenshot(png).toMatchBaseline("player/playing.png", {
|
|
168
|
+
mask: [
|
|
169
|
+
{ x: 1600, y: 1000, width: 300, height: 60 }, // time counter
|
|
170
|
+
{ x: 0, y: 1040, width: 1920, height: 40 } // progress bar
|
|
171
|
+
]
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Alignment tolerance
|
|
176
|
+
|
|
177
|
+
Some surfaces are drawn by firmware rather than by your app — the
|
|
178
|
+
system OSD raised by `StandardKeyboardDialog` and
|
|
179
|
+
`StandardPinPadDialog`, for example. Their internal row heights round
|
|
180
|
+
differently per device and firmware, so the whole surface re-centers by
|
|
181
|
+
1-3px between otherwise-identical runs. Every separator line and text
|
|
182
|
+
row then lands a pixel or two off, which is easily enough to blow a 1%
|
|
183
|
+
budget even though nothing about the app changed.
|
|
184
|
+
|
|
185
|
+
`alignTolerance` lets the comparator translate the capture by a few
|
|
186
|
+
whole pixels before scoring it:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
await expectScreenshot(png).toMatchBaseline("dialogs/keyboard-open.png", {
|
|
190
|
+
threshold: 0.01,
|
|
191
|
+
mask: KEYBOARD_MASK,
|
|
192
|
+
alignTolerance: { y: 3 } // absorb vertical re-centering
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Three properties make this safe to reach for:
|
|
197
|
+
|
|
198
|
+
- **It can only rescue a failing assertion, never relax a passing
|
|
199
|
+
one.** The untranslated comparison runs first; the search happens only
|
|
200
|
+
if that already exceeded `threshold`. A test that passes today keeps
|
|
201
|
+
passing with byte-identical semantics, and the happy path does no
|
|
202
|
+
extra work.
|
|
203
|
+
- **`threshold` still applies in full** at the winning offset. The
|
|
204
|
+
tolerance buys positional slack, not a bigger mismatch budget, so a
|
|
205
|
+
missing row or a wrong color still fails.
|
|
206
|
+
- **The window is capped** at `ALIGN_TOLERANCE_MAX_PX` (4) per axis.
|
|
207
|
+
Needing more than that means the content genuinely differs and the
|
|
208
|
+
baseline should be re-captured, not slid into place.
|
|
209
|
+
|
|
210
|
+
Only the overlapping region is compared: at `dy=2` the top or bottom
|
|
211
|
+
two rows have no counterpart and are dropped from both the mismatch
|
|
212
|
+
count and the denominator.
|
|
213
|
+
|
|
214
|
+
Use this only for firmware-drawn chrome. App-rendered content is
|
|
215
|
+
positionally deterministic, and reaching for `alignTolerance` there
|
|
216
|
+
hides real layout regressions.
|
|
217
|
+
|
|
218
|
+
When the assertion fails even after aligning, the error names the best
|
|
219
|
+
offset it found and the untranslated ratio, which distinguishes "the
|
|
220
|
+
surface drifted further than the tolerance" from "the content is
|
|
221
|
+
genuinely different":
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
... (unmasked ratio 0.00412 > threshold 0.001). Best alignment dx=0, dy=2
|
|
225
|
+
(untranslated ratio was 0.01027). Diff: .../keyboard-open.diff.png.
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
A malformed tolerance (non-integer, negative, or over the cap) throws
|
|
229
|
+
`ScreenshotAlignToleranceError`. Validation runs up front, so a typo
|
|
230
|
+
surfaces on a passing run too rather than lying dormant until the day
|
|
231
|
+
the assertion drifts.
|
|
232
|
+
|
|
233
|
+
## Diff PNGs
|
|
234
|
+
|
|
235
|
+
On mismatch, the SDK renders a side-by-side diff PNG under
|
|
236
|
+
`getScreenshotDiffRoot()` (default `<cwd>/test-results/diffs/`). The
|
|
237
|
+
filename flattens the baseline path:
|
|
238
|
+
|
|
239
|
+
- `home/initial.png` → `home--initial.diff.png`
|
|
240
|
+
- `grid/after-select.png` → `grid--after-select.diff.png`
|
|
241
|
+
|
|
242
|
+
This keeps every failure's artifact in a single flat directory, easy
|
|
243
|
+
for CI reporters to upload. The `ScreenshotMismatchError` exposes the
|
|
244
|
+
full path in its `diffPath` field.
|
|
245
|
+
|
|
246
|
+
Diff PNGs are always full canvas size with baseline-aligned
|
|
247
|
+
coordinates, so a pixel in the diff names the same position as that
|
|
248
|
+
pixel in the baseline. When an `alignTolerance` search won, the rows or
|
|
249
|
+
columns that had no counterpart to compare are left transparent.
|
|
250
|
+
|
|
251
|
+
## Diff-root override
|
|
252
|
+
|
|
253
|
+
The diff-root is configured through `installDeviceTestHooks`:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import {
|
|
257
|
+
createDefaultClient,
|
|
258
|
+
installDeviceTestHooks
|
|
259
|
+
} from "@roku-sdk/device-test-client";
|
|
260
|
+
import { resolve } from "node:path";
|
|
261
|
+
|
|
262
|
+
const client = createDefaultClient();
|
|
263
|
+
|
|
264
|
+
installDeviceTestHooks({
|
|
265
|
+
client,
|
|
266
|
+
diffRoot: resolve(__dirname, "../../test-results/diffs")
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Internal / deep-import.** The underlying
|
|
271
|
+
`setScreenshotDiffRoot(absPath)` / `getScreenshotDiffRoot()`
|
|
272
|
+
functions are no longer re-exported from the barrel in v2.0.0 — the
|
|
273
|
+
public path is `installDeviceTestHooks({ diffRoot })`. If a bespoke
|
|
274
|
+
runner needs to set the diff root without using
|
|
275
|
+
`installDeviceTestHooks`, the functions are reachable via
|
|
276
|
+
`@roku-sdk/device-test-client/src/assertions/screenshot-artifacts`
|
|
277
|
+
(deep import, not SemVer-covered).
|
|
278
|
+
|
|
279
|
+
## `UPDATE_BASELINES=1` refresh
|
|
280
|
+
|
|
281
|
+
Re-run the suite with the env var set to regenerate baselines after a
|
|
282
|
+
legitimate UI change:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
UPDATE_BASELINES=1 pnpm jest path/to/file.test.ts
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
- Missing baseline → created from the actual PNG.
|
|
289
|
+
- Mismatched baseline → overwritten with the actual PNG.
|
|
290
|
+
**No diff PNG is written in the update path.**
|
|
291
|
+
- Dimensions mismatch → overwritten with the actual PNG.
|
|
292
|
+
- CI guard still applies — `CI=true` blocks writes.
|
|
293
|
+
|
|
294
|
+
## Errors thrown
|
|
295
|
+
|
|
296
|
+
- `ScreenshotBaselineMissingError` — no baseline + no
|
|
297
|
+
`UPDATE_BASELINES=1`. Fields: `baselinePath`, `resolution`.
|
|
298
|
+
- `ScreenshotDimensionsMismatchError` — baseline and actual disagree
|
|
299
|
+
on size. Fields: `baselinePath`, `resolution`, `baseline.w/h`,
|
|
300
|
+
`actual.w/h`.
|
|
301
|
+
- `ScreenshotMismatchError` — pixel diff over threshold. Fields:
|
|
302
|
+
`baselinePath`, `resolution`, `actual.w/h`, `mismatchedPixels`,
|
|
303
|
+
`totalPixels`, `ratio`, `threshold`, `diffPath`, and `alignment`
|
|
304
|
+
(`{ dx, dy }`, present only when an `alignTolerance` search improved on
|
|
305
|
+
the untranslated comparison).
|
|
306
|
+
- `ScreenshotNotStableError` — `captureStableScreenshot` timed out.
|
|
307
|
+
Fields: `iterations`, `lastDiffCount`, `firstDiffPoint`,
|
|
308
|
+
`maskedRegions`, `lastPng`.
|
|
309
|
+
- `ScreenshotMaskError` — invalid `MaskRegion`. Fields: `mask`,
|
|
310
|
+
`reason`.
|
|
311
|
+
- `ScreenshotAlignToleranceError` — invalid `alignTolerance` axis.
|
|
312
|
+
Fields: `axis`, `value`, `max`, `reason`.
|
|
313
|
+
- `AppCrashedError` — crash probe during stable-capture polling.
|
|
314
|
+
|
|
315
|
+
Full field lists and recovery advice in
|
|
316
|
+
`[errors.md](errors.md)`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@roku-sdk/device-test-client/internal/constants` — config-load-safe
|
|
6
|
+
* entry point for the small subset of `src/constants.ts` symbols that
|
|
7
|
+
* Jest configs (and other CommonJS-mode loaders) legitimately need at
|
|
8
|
+
* **parse time**.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists, and why the public barrel is not enough:
|
|
11
|
+
*
|
|
12
|
+
* `src/index.ts` re-exports `RkClient` from `src/client/client.ts`,
|
|
13
|
+
* which imports `@roku-sdk/tooling/src/device-test-helpers.ts`. The
|
|
14
|
+
* `@roku-sdk/tooling` package declares `"type": "module"` and uses
|
|
15
|
+
* `import.meta.url`. Loaders that parse `*.config.ts` in CommonJS
|
|
16
|
+
* mode (ts-node default, esbuild-register, the loader Jest invokes
|
|
17
|
+
* from `jest-config/readConfigFileAndSetRootDir`) blow up on the
|
|
18
|
+
* first hop with `ERR_REQUIRE_ESM` — *before* any test, hook, or
|
|
19
|
+
* transform runs. The stack trace points at the loader, not at the
|
|
20
|
+
* import that pulled the ESM file in, so the failure is opaque to
|
|
21
|
+
* the reader of `jest.config.ts`.
|
|
22
|
+
*
|
|
23
|
+
* `src/constants.ts` itself has only `type` imports (all erased by
|
|
24
|
+
* TypeScript) and value-level constants, so a file that imports
|
|
25
|
+
* exclusively from it is parse-safe under CJS. This subpath is the
|
|
26
|
+
* one sanctioned "deep import past the barrel" surface for that
|
|
27
|
+
* purpose.
|
|
28
|
+
*
|
|
29
|
+
* Why a re-export shim instead of a direct entry in `exports`:
|
|
30
|
+
* `device-tests/tsconfig.json` uses `moduleResolution: "Node"`
|
|
31
|
+
* (classic), which falls back to a filesystem walk and does NOT honor
|
|
32
|
+
* the package `exports` map. A physical file at this path resolves
|
|
33
|
+
* under every resolution mode; the `exports` map entry covers the
|
|
34
|
+
* publish-time / `Node16` / `Bundler` case symmetrically. The same
|
|
35
|
+
* pattern is used by `./internal/deploy`.
|
|
36
|
+
*
|
|
37
|
+
* SemVer / stability contract:
|
|
38
|
+
* Symbols here are reachable, typed, and stable across patch releases
|
|
39
|
+
* — but explicitly NOT covered by SemVer (`/internal/` in the path is
|
|
40
|
+
* the tell). Partner code MUST NOT import from this subpath; the
|
|
41
|
+
* public, SemVer-covered surface for these constants stays
|
|
42
|
+
* `@roku-sdk/device-test-client` (curated barrel in `src/index.ts`).
|
|
43
|
+
*
|
|
44
|
+
* Adding new symbols:
|
|
45
|
+
* Only constants that a config-loader truly needs at parse time
|
|
46
|
+
* belong here (file-name constants, env-var names, default budgets
|
|
47
|
+
* referenced by `jest.config.ts` reporters / global setup). Pure-
|
|
48
|
+
* implementation magic numbers stay file-internal in
|
|
49
|
+
* `src/constants.ts`. Prefer extending this barrel over minting a
|
|
50
|
+
* parallel `/internal/<other>` subpath — keeps the internal contract
|
|
51
|
+
* grep-able.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
export { ARTIFACT_FILE } from "../src/constants";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@roku-sdk/device-test-client/internal/deploy` — explicit internal
|
|
6
|
+
* subpath for the **deploy-side** tooling helpers that the curated
|
|
7
|
+
* public API intentionally does NOT carry.
|
|
8
|
+
*
|
|
9
|
+
* Currently exposes:
|
|
10
|
+
*
|
|
11
|
+
* - `resolveAppPath` — integration-test deploy-guard directory-wipe
|
|
12
|
+
* path resolution.
|
|
13
|
+
* - `invalidateBuildCacheForApp` — release the in-process
|
|
14
|
+
* `RkClient.buildAndDeploy` build memoisation entries scoped to
|
|
15
|
+
* a single `absRoot`. Pairs with the deploy-guard's
|
|
16
|
+
* `wipeIntegrationBuildOutput` so a "we rebuilt because the
|
|
17
|
+
* previous bundle was dirty" recovery path also invalidates the
|
|
18
|
+
* "we already built this" memoisation. Both calls must run in
|
|
19
|
+
* lockstep; calling one without the other is a bug.
|
|
20
|
+
*
|
|
21
|
+
* Symbols here are reachable, typed, and stable across patch
|
|
22
|
+
* releases — but explicitly NOT covered by SemVer (`/internal/` in
|
|
23
|
+
* the path is the tell). Partner code MUST NOT import from this
|
|
24
|
+
* subpath; downstream consumers should treat it as private.
|
|
25
|
+
*
|
|
26
|
+
* Why a re-export shim instead of a direct entry in `exports`:
|
|
27
|
+
* `device-tests/tsconfig.json` uses `moduleResolution: "Node"`
|
|
28
|
+
* (classic), which falls back to a filesystem walk and does NOT honor
|
|
29
|
+
* the package `exports` map. A physical file at this path resolves
|
|
30
|
+
* under every resolution mode; the `exports` map entry covers the
|
|
31
|
+
* publish-time / `Node16` / `Bundler` case symmetrically.
|
|
32
|
+
*
|
|
33
|
+
* Adding new symbols (load-chain caveat):
|
|
34
|
+
* This barrel re-exports from `../src/client/deploy`, which imports
|
|
35
|
+
* `@roku-sdk/tooling/src/device-test-helpers` — an ESM module
|
|
36
|
+
* (`"type": "module"`, `import.meta.url`). Anything importing this
|
|
37
|
+
* subpath therefore pulls that ESM chain in transitively. That is
|
|
38
|
+
* FINE for runtime callers (Jest transforms `@roku-sdk/*` via babel,
|
|
39
|
+
* see `device-tests/jest.config.ts → transform`) but it is FATAL
|
|
40
|
+
* for parse-time loaders such as ts-node in CJS mode that the IDE
|
|
41
|
+
* and Jest itself use to read `*.config.ts`; those crash with
|
|
42
|
+
* `ERR_REQUIRE_ESM` before any transform runs.
|
|
43
|
+
*
|
|
44
|
+
* Group new `@internal` symbols by **load-time safety**, not just
|
|
45
|
+
* by concern:
|
|
46
|
+
* - Runtime-only helpers (need the live client / build pipeline /
|
|
47
|
+
* tooling shim) → add here.
|
|
48
|
+
* - Symbols a `*.config.ts` legitimately reads at parse time →
|
|
49
|
+
* add to `./internal/constants` (or another sibling subpath
|
|
50
|
+
* whose source file imports only types and value-level
|
|
51
|
+
* literals). Re-confirm by tracing the new file's imports to
|
|
52
|
+
* leaves; one accidental hop into `@roku-sdk/tooling` re-breaks
|
|
53
|
+
* the config-loader path.
|
|
54
|
+
*
|
|
55
|
+
* Prefer extending an existing `/internal/<name>` subpath over
|
|
56
|
+
* minting a parallel one — keeps the internal contract grep-able.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
export { resolveAppPath } from "../src/client/deploy";
|
|
60
|
+
export { invalidateBuildCacheForApp } from "../src/client/build-cache";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@roku-sdk/device-test-client/internal/paths` — explicit internal
|
|
6
|
+
* subpath for load-safe path constants/helpers used by test config and
|
|
7
|
+
* setup code.
|
|
8
|
+
*
|
|
9
|
+
* This subpath intentionally re-exports only from `src/paths.ts`, whose
|
|
10
|
+
* runtime imports are Node built-ins. It is safe for CommonJS-mode config
|
|
11
|
+
* loaders (`jest.config.ts`, ts-node) and must stay free of `RkClient`,
|
|
12
|
+
* `@roku-sdk/tooling`, or other ESM-heavy runtime chains.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export { TEST_RESULTS_DIR_ENV_VAR, TEST_RESULTS_DIR_NAME, defaultTestResultsDir } from "../src/paths";
|
package/package.json
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roku-sdk/device-test-client",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.18.0",
|
|
4
|
+
"description": "Roku SDK on-device integration test client (RkClient, DOM/screenshot/log assertions, Jest wiring).",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./environment": {
|
|
13
|
+
"types": "./src/jest/device-test-environment.ts",
|
|
14
|
+
"default": "./src/jest/device-test-environment.ts"
|
|
15
|
+
},
|
|
16
|
+
"./internal/deploy": {
|
|
17
|
+
"types": "./internal/deploy.ts",
|
|
18
|
+
"default": "./internal/deploy.ts"
|
|
19
|
+
},
|
|
20
|
+
"./internal/constants": {
|
|
21
|
+
"types": "./internal/constants.ts",
|
|
22
|
+
"default": "./internal/constants.ts"
|
|
23
|
+
},
|
|
24
|
+
"./internal/paths": {
|
|
25
|
+
"types": "./internal/paths.ts",
|
|
26
|
+
"default": "./internal/paths.ts"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src/**/*",
|
|
32
|
+
"internal/**/*",
|
|
33
|
+
"docs/**/*",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24.0.0"
|
|
38
|
+
},
|
|
39
|
+
"author": "Roku Inc",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://gitlab.com/rokudev/rsg-sdk.git",
|
|
44
|
+
"directory": "packages/device-test-client"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://gitlab.com/rokudev/rsg-sdk/-/tree/main/packages/device-test-client#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://gitlab.com/rokudev/rsg-sdk/-/issues"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"pixelmatch": "^5.3.0",
|
|
55
|
+
"pngjs": "^7.0.0",
|
|
56
|
+
"semver": "^7.6.0",
|
|
57
|
+
"@roku-sdk/tooling": "2.18.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"jest-environment-node": ">=29.7.0",
|
|
61
|
+
"@roku-sdk/tooling": "^2.18.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/jest": "^30.0.0",
|
|
65
|
+
"@types/node": "^20.0.0",
|
|
66
|
+
"@types/pixelmatch": "^5.2.6",
|
|
67
|
+
"@types/pngjs": "^6.0.5",
|
|
68
|
+
"@types/semver": "^7.5.8",
|
|
69
|
+
"fast-check": "^4.7.0",
|
|
70
|
+
"jest": "^30.4.1",
|
|
71
|
+
"jest-environment-node": "^30.4.1",
|
|
72
|
+
"ts-jest": "^29.4.6",
|
|
73
|
+
"typescript": "5.5.4",
|
|
74
|
+
"ulid": "^2.3.0",
|
|
75
|
+
"@roku-sdk/dev-tools": "2.18.0",
|
|
76
|
+
"@roku-sdk/tooling": "2.18.0"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"test": "jest",
|
|
80
|
+
"lint": "eslint --no-error-on-unmatched-pattern \"src/**/*.{ts,tsx,mjs,cjs,js}\" \"__tests__/**/*.{ts,tsx,mjs,cjs,js}\"",
|
|
81
|
+
"typecheck": "tsc --noEmit"
|
|
82
|
+
}
|
|
6
83
|
}
|