@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,141 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Async device IP resolution. Split from the sync resolver so
|
|
5
|
+
* `createDefaultClient` never transitively loads
|
|
6
|
+
* `@roku-sdk/tooling`'s ESM `devices-store` (top-level
|
|
7
|
+
* `import.meta.url`) under CommonJS consumers like ts-jest.
|
|
8
|
+
*
|
|
9
|
+
* Flow: `globalSetup` calls `resolveActiveDeviceIp()` and persists
|
|
10
|
+
* via `persistDeviceIp`; worker processes read the persisted IP
|
|
11
|
+
* synchronously through `device-resolver-sync.ts`.
|
|
12
|
+
*
|
|
13
|
+
* Resolution precedence (later wins on conflict):
|
|
14
|
+
* 0. (Layer 0) `RK_DEVICE_LEASE_ID` — read the lease file
|
|
15
|
+
* `.roku/leases/<id>.json` and use its `ip`. This is the
|
|
16
|
+
* canonical CI path for provider-backed pools; the provider writes the
|
|
17
|
+
* lease at acquire-time and pins the consumer at boot.
|
|
18
|
+
* 1. `RK_DEVICE_IP` — partner-side override. Still WINS on
|
|
19
|
+
* conflict so existing partner pipelines that pin an IP
|
|
20
|
+
* keep working byte-for-byte after this file lands.
|
|
21
|
+
* 2. `.roku/` active device → first registered device.
|
|
22
|
+
*
|
|
23
|
+
* IMPORTANT: `RK_DEVICE_IP` deliberately wins over a lease file.
|
|
24
|
+
* Rationale:
|
|
25
|
+
* - Partner pipelines have set `RK_DEVICE_IP` for years; flipping
|
|
26
|
+
* precedence would silently break them when an unrelated lease
|
|
27
|
+
* file from a prior interactive `rk device lease acquire` is
|
|
28
|
+
* still on disk.
|
|
29
|
+
* - The internal CI path always sets `RK_DEVICE_LEASE_ID` AND
|
|
30
|
+
* leaves `RK_DEVICE_IP` unset, so the lease-file path "wins"
|
|
31
|
+
* in practice without any precedence ambiguity.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { promises as fs } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
|
|
37
|
+
import { leaseFilePath, readLeaseFile } from "@roku-sdk/tooling/src/device-pool/lease-writer";
|
|
38
|
+
import { loadListMergedDevices } from "@roku-sdk/tooling/src/device-test-helpers";
|
|
39
|
+
|
|
40
|
+
import { DEVICE_IP_CACHE_FILE, ENV_VAR } from "../constants";
|
|
41
|
+
import { RkClientError } from "../errors";
|
|
42
|
+
|
|
43
|
+
// `loadListMergedDevices` lives on the tooling side (in
|
|
44
|
+
// `@roku-sdk/tooling/device-test-helpers`) so the deep-require path
|
|
45
|
+
// is owned by the package whose internals it references. Static
|
|
46
|
+
// import of `devices-store` would pull in `get-config.ts` (top-level
|
|
47
|
+
// `import.meta.url`) which is illegal under our CommonJS tsconfig;
|
|
48
|
+
// the helper defers resolution to runtime via `require()`.
|
|
49
|
+
|
|
50
|
+
export { resolveDeviceIpSync } from "./device-resolver-sync";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Layer 0 lookup: read the lease file the device-pool provider wrote
|
|
54
|
+
* for this job and return its `ip` if usable.
|
|
55
|
+
*
|
|
56
|
+
* Returns `undefined` (not throws) on every recoverable miss — env
|
|
57
|
+
* unset, lease dir missing, file corrupt — so a higher precedence
|
|
58
|
+
* layer (`RK_DEVICE_IP`) or a lower one (`.roku/` active) can still
|
|
59
|
+
* resolve a valid IP. The one fatal case is "env says use lease X
|
|
60
|
+
* but no other layer has an IP either"; that surfaces at the bottom
|
|
61
|
+
* of `resolveActiveDeviceIp` as the existing `RK_DEVICE_IP_UNRESOLVED`
|
|
62
|
+
* error, which already has the actionable remediation steps.
|
|
63
|
+
*
|
|
64
|
+
* We deliberately do NOT mutate `.roku/roku-dev-config.json` or
|
|
65
|
+
* `rk-server` state — the lease file is the single source of truth,
|
|
66
|
+
* and `rk-server` (when started in a job with `RK_DEVICE_LEASE_ID`
|
|
67
|
+
* set) reads through the same file directly. This is what makes the
|
|
68
|
+
* parallel-CI model safe: workspace-singleton state cannot race.
|
|
69
|
+
*/
|
|
70
|
+
async function resolveFromLeaseFile(cwd: string): Promise<string | undefined> {
|
|
71
|
+
const leaseId = process.env[ENV_VAR.RK_DEVICE_LEASE_ID];
|
|
72
|
+
if (!leaseId || leaseId.length === 0) return undefined;
|
|
73
|
+
const leaseDir = process.env[ENV_VAR.RK_DEVICE_LEASE_DIR] ?? join(cwd, ".roku", "leases");
|
|
74
|
+
try {
|
|
75
|
+
const lease = await readLeaseFile(leaseId, leaseDir);
|
|
76
|
+
if (typeof lease.ip === "string" && lease.ip.length > 0) {
|
|
77
|
+
return lease.ip;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
} catch {
|
|
81
|
+
// `readLeaseFile` throws `DevicePoolError(INVALID_LEASE_FILE)`
|
|
82
|
+
// on ENOENT / malformed JSON / schema mismatch. We treat that
|
|
83
|
+
// as "Layer 0 didn't resolve" and let the higher / lower
|
|
84
|
+
// layers continue. A truly broken lease file is logged louder
|
|
85
|
+
// by the CLI verbs that wrote it (acquire/reap); the resolver
|
|
86
|
+
// is not the right place to surface that.
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the device IP. See file-level doc-block for the precedence
|
|
93
|
+
* order. Throws `RkClientError(RK_DEVICE_IP_UNRESOLVED)` with an
|
|
94
|
+
* actionable message when no layer yields an IP.
|
|
95
|
+
*/
|
|
96
|
+
export async function resolveActiveDeviceIp(cwd: string = process.cwd()): Promise<string> {
|
|
97
|
+
const envIp = process.env[ENV_VAR.RK_DEVICE_IP];
|
|
98
|
+
if (envIp && envIp.length > 0) return envIp;
|
|
99
|
+
|
|
100
|
+
const leaseIp = await resolveFromLeaseFile(cwd);
|
|
101
|
+
if (leaseIp) return leaseIp;
|
|
102
|
+
|
|
103
|
+
const listMergedDevices = loadListMergedDevices();
|
|
104
|
+
const { devices, activeId, activeRceId } = await listMergedDevices(cwd);
|
|
105
|
+
if (activeRceId) {
|
|
106
|
+
throw new RkClientError(
|
|
107
|
+
`The active device ${activeRceId} is a Roku Cloud Emulator device; device tests run on LAN devices only. ` +
|
|
108
|
+
`Set ${ENV_VAR.RK_DEVICE_IP}=<ip> or run \`rk device set-active <lan-device>\`.`,
|
|
109
|
+
"DEVICE_KIND_UNSUPPORTED"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (activeId) {
|
|
113
|
+
const active = devices.find(d => d.id === activeId);
|
|
114
|
+
if (active?.ip) return active.ip;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (devices.length > 0 && devices[0]?.ip) return devices[0].ip;
|
|
118
|
+
|
|
119
|
+
throw new RkClientError(
|
|
120
|
+
`Unable to resolve a Roku device IP. Set ${ENV_VAR.RK_DEVICE_IP}=<ip>, ` +
|
|
121
|
+
`set ${ENV_VAR.RK_DEVICE_LEASE_ID}=<leaseId> after \`rk device lease acquire --target <ipv4>\` ` +
|
|
122
|
+
`(no \`rk device add\` required for raw IPs), or run \`rk device add <ip>\` in the workspace, ` +
|
|
123
|
+
`then re-run the suite. IPv6 and hostnames are not supported by the local pool.`,
|
|
124
|
+
"RK_DEVICE_IP_UNRESOLVED"
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Re-export `leaseFilePath` so consumers (e.g. `rk-server`) that
|
|
129
|
+
// need to construct the same path as the resolver have a single
|
|
130
|
+
// source of truth. Avoids two implementations drifting on
|
|
131
|
+
// `.roku/leases/<id>.json` layout.
|
|
132
|
+
export { leaseFilePath };
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write the resolved IP into `testResultsDir/.device-ip` so worker
|
|
136
|
+
* processes (spawned after globalSetup) can read it synchronously.
|
|
137
|
+
*/
|
|
138
|
+
export async function persistDeviceIp(testResultsDir: string, ip: string): Promise<void> {
|
|
139
|
+
await fs.mkdir(testResultsDir, { recursive: true });
|
|
140
|
+
await fs.writeFile(join(testResultsDir, DEVICE_IP_CACHE_FILE), ip, "utf8");
|
|
141
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Provisioning facts the device-pool provider recorded on the lease.
|
|
5
|
+
*
|
|
6
|
+
* These describe how the device was *prepared*, which no ECP query can
|
|
7
|
+
* answer: `/query/device-info` reports the running `ui-software` version
|
|
8
|
+
* but not which OS image produced it. `globalSetup` folds them into the
|
|
9
|
+
* `device-env.json` artifact so a red CI run carries its own provenance.
|
|
10
|
+
*
|
|
11
|
+
* The lease file — not an env var — is the source of truth. Provider-owned
|
|
12
|
+
* env vars are Roku-internal and deliberately unknown to this package; the
|
|
13
|
+
* lease is the generic, already-sanctioned hand-off (the same one
|
|
14
|
+
* `device-resolver.ts` reads the IP from), and it records what the provider
|
|
15
|
+
* *actually* booted rather than what CI *asked* for.
|
|
16
|
+
*
|
|
17
|
+
* Every miss degrades to `{}`: no lease (local desk run), a provider that
|
|
18
|
+
* flashes nothing (`local`, say), a corrupt file, or an older provider
|
|
19
|
+
* that predates the rider. The artifact simply omits the field — absence
|
|
20
|
+
* means "unknown", never "none".
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
import { readLeaseFile } from "@roku-sdk/tooling/src/device-pool/lease-writer";
|
|
26
|
+
|
|
27
|
+
import { ENV_VAR, LEASE_FIELD } from "../constants";
|
|
28
|
+
|
|
29
|
+
export interface LeaseProvisioning {
|
|
30
|
+
/** See `LEASE_FIELD.ROOTFS_ARTIFACTORY_PATH`. Absent when unknown. */
|
|
31
|
+
rootfsArtifactoryPath?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read the provisioning riders off this job's lease. Never throws — a
|
|
36
|
+
* diagnostics-only artifact field must not be able to fail a suite that
|
|
37
|
+
* would otherwise pass.
|
|
38
|
+
*/
|
|
39
|
+
export async function readLeaseProvisioning(cwd: string = process.cwd()): Promise<LeaseProvisioning> {
|
|
40
|
+
const leaseId = process.env[ENV_VAR.RK_DEVICE_LEASE_ID];
|
|
41
|
+
if (!leaseId || leaseId.length === 0) return {};
|
|
42
|
+
const leaseDir = process.env[ENV_VAR.RK_DEVICE_LEASE_DIR] ?? join(cwd, ".roku", "leases");
|
|
43
|
+
try {
|
|
44
|
+
const lease = await readLeaseFile(leaseId, leaseDir);
|
|
45
|
+
const rootfs = (lease as Record<string, unknown>)[LEASE_FIELD.ROOTFS_ARTIFACTORY_PATH];
|
|
46
|
+
return typeof rootfs === "string" && rootfs.length > 0 ? { rootfsArtifactoryPath: rootfs } : {};
|
|
47
|
+
} catch {
|
|
48
|
+
// `readLeaseFile` throws on ENOENT / malformed JSON / schema
|
|
49
|
+
// mismatch. The IP resolver already ran against the same file and
|
|
50
|
+
// reports a genuinely broken lease far more loudly than a
|
|
51
|
+
// provenance field ever should, so stay silent here.
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|