@putdotio/rokit 1.1.0 → 1.2.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/README.md +8 -4
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +1 -1
- package/dist/rokit.mjs +1 -1
- package/dist/{roku-D-FxlWPE.mjs → roku-DxlYPbY1.mjs} +37 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ const context: RokuContext = {
|
|
|
63
63
|
|
|
64
64
|
await pressKey(context, "Info");
|
|
65
65
|
await assertSceneGraphNode(context, "videoPlayerScreen", { state: "visible" });
|
|
66
|
-
await querySceneGraph(context);
|
|
66
|
+
await querySceneGraph(context, { attempts: 3 });
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## Commands
|
|
@@ -88,13 +88,17 @@ rokit --version
|
|
|
88
88
|
is reachable.
|
|
89
89
|
- `device-info` prints enhanced Roku device metadata as JSON.
|
|
90
90
|
- `active-app` prints the foreground app.
|
|
91
|
-
- `wait-active` waits until the requested app is foregrounded
|
|
91
|
+
- `wait-active` waits until the requested app is foregrounded and tolerates
|
|
92
|
+
transient ECP read failures while polling.
|
|
92
93
|
- `launch` opens an app and waits until it is active. Use repeated `--param`
|
|
93
|
-
values for deeplink parameters.
|
|
94
|
+
values for deeplink parameters. Roku launch responses can race app startup, so
|
|
95
|
+
launch accepts transient timeout/fetch failures and then verifies foreground
|
|
96
|
+
state.
|
|
94
97
|
- `press` sends Roku remote keys through ECP. Use `--delay-ms` for navigation
|
|
95
98
|
sequences that need a stable gap between keys.
|
|
96
99
|
- `query` prints a raw ECP response such as `/query/sgnodes/all`.
|
|
97
|
-
- `sgnodes` prints the raw SceneGraph tree from `/query/sgnodes/all`.
|
|
100
|
+
- `sgnodes` prints the raw SceneGraph tree from `/query/sgnodes/all`. Library
|
|
101
|
+
callers can pass retry options to `querySceneGraph`.
|
|
98
102
|
- `assert-node` checks a named SceneGraph node once.
|
|
99
103
|
- `wait-node` polls SceneGraph until a named node condition matches.
|
|
100
104
|
- `screenshot` saves a developer screenshot. It requires `ROKIT_PASSWORD`.
|
package/dist/index.d.mts
CHANGED
|
@@ -42,6 +42,10 @@ type DeviceSummary = {
|
|
|
42
42
|
readonly model: string;
|
|
43
43
|
readonly name: string;
|
|
44
44
|
};
|
|
45
|
+
type RetryOptions = {
|
|
46
|
+
readonly attempts?: number;
|
|
47
|
+
readonly retryDelayMs?: number;
|
|
48
|
+
};
|
|
45
49
|
declare const checkDevice: (context: RokuContext) => Promise<DeviceSummary>;
|
|
46
50
|
declare const getDeviceInfo: (context: RokuContext) => Promise<rokuDeploy.DeviceInfo>;
|
|
47
51
|
declare const queryActiveApp: (context: RokuContext) => Promise<ActiveApp>;
|
|
@@ -49,7 +53,7 @@ declare const waitForActiveApp: (context: RokuContext, appId: string, timeoutMs?
|
|
|
49
53
|
declare const launchApp: (context: RokuContext, appId: string, params?: ReadonlyMap<string, string>) => Promise<ActiveApp>;
|
|
50
54
|
declare const pressKey: (context: RokuContext, key: string) => Promise<void>;
|
|
51
55
|
declare const queryEcp: (context: RokuContext, path: string) => Promise<string>;
|
|
52
|
-
declare const querySceneGraph: (context: RokuContext) => Promise<string>;
|
|
56
|
+
declare const querySceneGraph: (context: RokuContext, options?: RetryOptions) => Promise<string>;
|
|
53
57
|
declare const assertSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation) => Promise<void>;
|
|
54
58
|
declare const waitForSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation, timeoutMs?: number) => Promise<void>;
|
|
55
59
|
declare const installPackage: (context: RokuContext & {
|
|
@@ -60,4 +64,4 @@ declare const takeScreenshot: (context: RokuContext & {
|
|
|
60
64
|
}, outputPath: string) => Promise<string>;
|
|
61
65
|
declare const validateRemoteKey: (key: string) => void;
|
|
62
66
|
//#endregion
|
|
63
|
-
export { type ActiveApp, type DeviceSummary, type NodeExpectation, type NodeState, type RemoteKey, type RokuContext, assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
|
|
67
|
+
export { type ActiveApp, type DeviceSummary, type NodeExpectation, type NodeState, type RemoteKey, type RetryOptions, type RokuContext, assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { S as readXmlTag, _ as isNamedNodeVisible, a as launchApp, b as readActiveApp, c as queryEcp, d as validateRemoteKey, f as waitForActiveApp, g as assertNamedNodeText, h as assertNamedNodeState, i as installPackage, l as querySceneGraph, m as assertNamedNode, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot, v as readNamedNodeAttribute, x as readXmlAttribute, y as readNamedNodeAttributes } from "./roku-
|
|
1
|
+
import { S as readXmlTag, _ as isNamedNodeVisible, a as launchApp, b as readActiveApp, c as queryEcp, d as validateRemoteKey, f as waitForActiveApp, g as assertNamedNodeText, h as assertNamedNodeState, i as installPackage, l as querySceneGraph, m as assertNamedNode, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot, v as readNamedNodeAttribute, x as readXmlAttribute, y as readNamedNodeAttributes } from "./roku-DxlYPbY1.mjs";
|
|
2
2
|
export { assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
|
package/dist/rokit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as launchApp, c as queryEcp, f as waitForActiveApp, i as installPackage, l as querySceneGraph, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot } from "./roku-
|
|
2
|
+
import { a as launchApp, c as queryEcp, f as waitForActiveApp, i as installPackage, l as querySceneGraph, n as checkDevice, o as pressKey, p as waitForSceneGraphNode, r as getDeviceInfo, s as queryActiveApp, t as assertSceneGraphNode, u as takeScreenshot } from "./roku-DxlYPbY1.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { existsSync, mkdirSync } from "node:fs";
|
|
@@ -110,18 +110,27 @@ const queryActiveApp = async (context) => readActiveApp(await fetchText(context,
|
|
|
110
110
|
const waitForActiveApp = async (context, appId, timeoutMs = 1e4) => {
|
|
111
111
|
const start = Date.now();
|
|
112
112
|
let lastApp;
|
|
113
|
+
let lastError;
|
|
113
114
|
while (Date.now() - start < timeoutMs) {
|
|
114
|
-
|
|
115
|
+
try {
|
|
116
|
+
lastApp = await queryActiveApp(context);
|
|
117
|
+
lastError = void 0;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
lastError = formatErrorMessage(error);
|
|
120
|
+
await sleep(500);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
115
123
|
if (lastApp.id === appId) return lastApp;
|
|
116
124
|
await sleep(500);
|
|
117
125
|
}
|
|
118
126
|
const last = lastApp ? `${lastApp.id} ${lastApp.name}` : "unknown";
|
|
119
|
-
|
|
127
|
+
const errorSuffix = lastError ? `; last ECP error: ${lastError}` : "";
|
|
128
|
+
throw new Error(`expected active app ${appId}, got ${last}${errorSuffix}`);
|
|
120
129
|
};
|
|
121
130
|
const launchApp = async (context, appId, params = /* @__PURE__ */ new Map()) => {
|
|
122
131
|
const url = ecpUrl(context, `/launch/${encodeURIComponent(appId)}`);
|
|
123
132
|
for (const [key, value] of params) url.searchParams.set(key, value);
|
|
124
|
-
await
|
|
133
|
+
await postLaunchMaybeAccepted(context, url);
|
|
125
134
|
return await waitForActiveApp(context, appId);
|
|
126
135
|
};
|
|
127
136
|
const pressKey = async (context, key) => {
|
|
@@ -129,7 +138,18 @@ const pressKey = async (context, key) => {
|
|
|
129
138
|
await postOk(context, ecpUrl(context, `/keypress/${encodeURIComponent(key)}`));
|
|
130
139
|
};
|
|
131
140
|
const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
|
|
132
|
-
const querySceneGraph = async (context) =>
|
|
141
|
+
const querySceneGraph = async (context, options = {}) => {
|
|
142
|
+
const attempts = options.attempts ?? 1;
|
|
143
|
+
const retryDelayMs = options.retryDelayMs ?? 500;
|
|
144
|
+
let lastError;
|
|
145
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) try {
|
|
146
|
+
return await queryEcp(context, "/query/sgnodes/all");
|
|
147
|
+
} catch (error) {
|
|
148
|
+
lastError = error;
|
|
149
|
+
if (attempt < attempts - 1) await sleep(retryDelayMs);
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`SceneGraph query failed: ${formatErrorMessage(lastError)}`);
|
|
152
|
+
};
|
|
133
153
|
const assertSceneGraphNode = async (context, nodeName, expectation) => {
|
|
134
154
|
assertNamedNode(await querySceneGraph(context), nodeName, expectation);
|
|
135
155
|
};
|
|
@@ -189,9 +209,22 @@ const postOk = async (context, url) => {
|
|
|
189
209
|
});
|
|
190
210
|
if (!response.ok) throw new Error(`POST ${url.pathname} returned HTTP ${response.status}`);
|
|
191
211
|
};
|
|
212
|
+
const postLaunchMaybeAccepted = async (context, url) => {
|
|
213
|
+
try {
|
|
214
|
+
const response = await fetch(url, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
signal: AbortSignal.timeout(context.timeoutMs)
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok && response.status !== 503) throw new Error(`POST ${url.pathname} returned HTTP ${response.status}`);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const message = formatErrorMessage(error).toLowerCase();
|
|
221
|
+
if (!message.includes("abort") && !message.includes("timeout") && !message.includes("fetch failed")) throw error;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
192
224
|
const ecpUrl = (context, path) => new URL(path, `http://${context.target}:${ecpPort}`);
|
|
193
225
|
const sleep = (ms) => new Promise((resolve) => {
|
|
194
226
|
setTimeout(resolve, ms);
|
|
195
227
|
});
|
|
228
|
+
const formatErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
196
229
|
//#endregion
|
|
197
230
|
export { readXmlTag as S, isNamedNodeVisible as _, launchApp as a, readActiveApp as b, queryEcp as c, validateRemoteKey as d, waitForActiveApp as f, assertNamedNodeText as g, assertNamedNodeState as h, installPackage as i, querySceneGraph as l, assertNamedNode as m, checkDevice as n, pressKey as o, waitForSceneGraphNode as p, getDeviceInfo as r, queryActiveApp as s, assertSceneGraphNode as t, takeScreenshot as u, readNamedNodeAttribute as v, readXmlAttribute as x, readNamedNodeAttributes as y };
|