@putdotio/rokit 1.6.0 → 1.7.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 CHANGED
@@ -47,9 +47,11 @@ App-specific scenario scripts can also import the generic helpers:
47
47
  import {
48
48
  assertNamedNodeState,
49
49
  assertNamedNodeTranslation,
50
+ assertMediaPlayerContainer,
50
51
  assertSceneGraphNode,
51
52
  pressKey,
52
53
  querySceneGraph,
54
+ sceneGraphContainsText,
53
55
  waitForSceneGraphAssertion,
54
56
  type RokuContext,
55
57
  } from "@putdotio/rokit";
@@ -67,11 +69,15 @@ const context: RokuContext = {
67
69
  };
68
70
 
69
71
  await pressKey(context, "Info");
70
- const xml = await querySceneGraph(context, { attempts: 3 });
72
+ const xml = await querySceneGraph(context, { attempts: 3, requireComplete: true });
71
73
  await assertSceneGraphNode(context, "videoPlayerScreen", { state: "visible" });
72
74
  assertNamedNodeTranslation(xml, "videoPlayerScreen", 0, 0);
75
+ await assertMediaPlayerContainer(context, "mp4");
73
76
  await waitForSceneGraphAssertion(context, "expected player", (xml) => {
74
77
  assertNamedNodeState(xml, "videoPlayerScreen", "visible");
78
+ if (!sceneGraphContainsText(xml, "Ready")) {
79
+ throw new Error("expected ready text");
80
+ }
75
81
  });
76
82
  ```
77
83
 
@@ -123,7 +129,9 @@ and reports failures as `{ "status": "failed", "error": { "message": "..." } }`.
123
129
  sequences that need a stable gap between keys.
124
130
  - `query` prints a raw ECP response such as `/query/sgnodes/all`.
125
131
  - `sgnodes` prints the raw SceneGraph tree from `/query/sgnodes/all`. Library
126
- callers can pass retry options to `querySceneGraph`.
132
+ callers can pass retry options to `querySceneGraph`; use
133
+ `requireComplete: true` when a scenario needs to reject partial SceneGraph
134
+ dumps that include `<All_Nodes>` but no root `<App>` node yet.
127
135
  - `assert-node` checks a named SceneGraph node once.
128
136
  - `wait-node` polls SceneGraph until a named node condition matches.
129
137
  - `screenshot` saves a developer screenshot. It requires `ROKIT_PASSWORD`.
@@ -155,8 +163,10 @@ tokens, and app-specific media identifiers do not belong in git.
155
163
  - remote keypresses
156
164
  - raw ECP queries
157
165
  - parsed media-player state from `/query/media-player`
166
+ - media-player active-state and container assertions
158
167
  - SceneGraph state queries and named-node assertions
159
168
  - SceneGraph attribute, numeric geometry, bounds, and translation readers
169
+ - SceneGraph completeness and escaped-text helpers
160
170
  - SceneGraph geometry assertions, status/failure readers, and custom assertion
161
171
  wait loops
162
172
  - screenshots
package/dist/index.d.mts CHANGED
@@ -23,6 +23,9 @@ declare const readNamedNodeTranslation: (xml: string, nodeName: string) => Scene
23
23
  declare const readSceneGraphStatus: (xml: string) => SceneGraphStatus;
24
24
  declare const readSceneGraphFailure: (xml: string) => string | undefined;
25
25
  declare const isNamedNodeVisible: (xml: string, nodeName: string) => boolean;
26
+ declare const isCompleteSceneGraph: (xml: string) => boolean;
27
+ declare const escapeXmlAttribute: (value: string) => string;
28
+ declare const sceneGraphContainsText: (xml: string, text: string) => boolean;
26
29
  declare const assertSceneGraphNumberNear: (actual: number | undefined, expected: number, label: string, tolerance?: number) => void;
27
30
  declare const assertNamedNodeTranslation: (xml: string, nodeName: string, expectedX: number, expectedY: number, tolerance?: number) => void;
28
31
  declare const assertNamedNodeSize: (xml: string, nodeName: string, expectedWidth: number, expectedHeight: number, tolerance?: number) => void;
@@ -77,6 +80,7 @@ type MediaPlayerInfo = {
77
80
  };
78
81
  type RetryOptions = {
79
82
  readonly attempts?: number;
83
+ readonly requireComplete?: boolean;
80
84
  readonly retryDelayMs?: number;
81
85
  };
82
86
  type SceneGraphAssertion = (xml: string) => void;
@@ -93,11 +97,14 @@ declare const pressKey: (context: RokuContext, key: string) => Promise<void>;
93
97
  declare const queryEcp: (context: RokuContext, path: string) => Promise<string>;
94
98
  declare const queryMediaPlayerXml: (context: RokuContext) => Promise<string>;
95
99
  declare const queryMediaPlayer: (context: RokuContext) => Promise<MediaPlayerInfo>;
100
+ declare const queryMediaPlayerXmlSafe: (context: RokuContext) => Promise<string | undefined>;
101
+ declare const queryMediaPlayerSafe: (context: RokuContext) => Promise<MediaPlayerInfo | undefined>;
96
102
  declare const readMediaPlayerInfo: (xml: string) => MediaPlayerInfo;
97
103
  declare const readMediaPlayerState: (xml: string) => MediaPlayerState | undefined;
98
104
  declare const readMediaPlayerPositionMs: (xml: string) => number | undefined;
99
105
  declare const readMediaPlayerContainer: (xml: string) => string | undefined;
100
106
  declare const isActiveMediaPlayerState: (state: string | undefined) => boolean;
107
+ declare const assertMediaPlayerContainer: (context: RokuContext, expectedContainer: string) => Promise<MediaPlayerInfo>;
101
108
  declare const waitForMediaPlayerState: (context: RokuContext, expectedState: MediaPlayerState, timeoutMs?: number) => Promise<MediaPlayerInfo>;
102
109
  declare const querySceneGraph: (context: RokuContext, options?: RetryOptions) => Promise<string>;
103
110
  declare const assertSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation) => Promise<void>;
@@ -111,4 +118,4 @@ declare const takeScreenshot: (context: RokuContext & {
111
118
  }, outputPath: string) => Promise<string>;
112
119
  declare const validateRemoteKey: (key: string) => void;
113
120
  //#endregion
114
- export { type ActiveApp, type DeviceSummary, type MediaPlayerInfo, type MediaPlayerState, type NodeExpectation, type NodeState, type RemoteKey, type RetryOptions, type RokuContext, type SceneGraphAssertion, type SceneGraphBounds, type SceneGraphPoint, type SceneGraphStatus, type WaitForSceneGraphAssertionOptions, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, getDeviceInfo, installPackage, isActiveMediaPlayerState, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerXml, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
121
+ export { type ActiveApp, type DeviceSummary, type MediaPlayerInfo, type MediaPlayerState, type NodeExpectation, type NodeState, type RemoteKey, type RetryOptions, type RokuContext, type SceneGraphAssertion, type SceneGraphBounds, type SceneGraphPoint, type SceneGraphStatus, type WaitForSceneGraphAssertionOptions, assertMediaPlayerContainer, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, escapeXmlAttribute, getDeviceInfo, installPackage, isActiveMediaPlayerState, isCompleteSceneGraph, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerSafe, queryMediaPlayerXml, queryMediaPlayerXmlSafe, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, sceneGraphContainsText, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { A as parseSceneGraphNumberList, B as readXmlTag, C as assertNamedNode, D as assertNamedNodeTranslation, E as assertNamedNodeText, F as readNamedNodeTranslation, I as readSceneGraphFailure, L as readSceneGraphStatus, M as readNamedNodeAttributes, N as readNamedNodeBounds, O as assertSceneGraphNumberNear, P as readNamedNodeNumber, R as readActiveApp, S as waitForSceneGraphNode, T as assertNamedNodeState, _ as takeScreenshot, a as isActiveMediaPlayerState, b as waitForMediaPlayerState, c as queryActiveApp, d as queryMediaPlayerXml, f as querySceneGraph, g as readMediaPlayerState, h as readMediaPlayerPositionMs, i as installPackage, j as readNamedNodeAttribute, k as isNamedNodeVisible, l as queryEcp, m as readMediaPlayerInfo, n as checkDevice, o as launchApp, p as readMediaPlayerContainer, r as getDeviceInfo, s as pressKey, t as assertSceneGraphNode, u as queryMediaPlayer, v as validateRemoteKey, w as assertNamedNodeSize, x as waitForSceneGraphAssertion, y as waitForActiveApp, z as readXmlAttribute } from "./roku-CXHAzYR-.mjs";
2
- export { assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, getDeviceInfo, installPackage, isActiveMediaPlayerState, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerXml, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
1
+ import { A as assertNamedNodeTranslation, B as readNamedNodeTranslation, C as waitForMediaPlayerState, D as assertNamedNodeSize, E as assertNamedNode, F as parseSceneGraphNumberList, G as readXmlAttribute, H as readSceneGraphStatus, I as readNamedNodeAttribute, K as readXmlTag, L as readNamedNodeAttributes, M as escapeXmlAttribute, N as isCompleteSceneGraph, O as assertNamedNodeState, P as isNamedNodeVisible, R as readNamedNodeBounds, S as waitForActiveApp, T as waitForSceneGraphNode, U as sceneGraphContainsText, V as readSceneGraphFailure, W as readActiveApp, _ as readMediaPlayerInfo, a as installPackage, b as takeScreenshot, c as pressKey, d as queryMediaPlayer, f as queryMediaPlayerSafe, g as readMediaPlayerContainer, h as querySceneGraph, i as getDeviceInfo, j as assertSceneGraphNumberNear, k as assertNamedNodeText, l as queryActiveApp, m as queryMediaPlayerXmlSafe, n as assertSceneGraphNode, o as isActiveMediaPlayerState, p as queryMediaPlayerXml, r as checkDevice, s as launchApp, t as assertMediaPlayerContainer, u as queryEcp, v as readMediaPlayerPositionMs, w as waitForSceneGraphAssertion, x as validateRemoteKey, y as readMediaPlayerState, z as readNamedNodeNumber } from "./roku-B61mpmWt.mjs";
2
+ export { assertMediaPlayerContainer, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, escapeXmlAttribute, getDeviceInfo, installPackage, isActiveMediaPlayerState, isCompleteSceneGraph, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerSafe, queryMediaPlayerXml, queryMediaPlayerXmlSafe, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, sceneGraphContainsText, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
package/dist/rokit.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as waitForSceneGraphNode, _ as takeScreenshot, b as waitForMediaPlayerState, c as queryActiveApp, f as querySceneGraph, i as installPackage, l as queryEcp, n as checkDevice, o as launchApp, r as getDeviceInfo, s as pressKey, t as assertSceneGraphNode, u as queryMediaPlayer, y as waitForActiveApp } from "./roku-CXHAzYR-.mjs";
2
+ import { C as waitForMediaPlayerState, S as waitForActiveApp, T as waitForSceneGraphNode, a as installPackage, b as takeScreenshot, c as pressKey, d as queryMediaPlayer, h as querySceneGraph, i as getDeviceInfo, l as queryActiveApp, n as assertSceneGraphNode, r as checkDevice, s as launchApp, u as queryEcp } from "./roku-B61mpmWt.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync } from "node:fs";
@@ -70,6 +70,9 @@ const isNamedNodeVisible = (xml, nodeName) => {
70
70
  const attributes = readNamedNodeAttributes(xml, nodeName);
71
71
  return attributes !== void 0 && !attributes.includes("visible=\"false\"");
72
72
  };
73
+ const isCompleteSceneGraph = (xml) => !xml.includes("<All_Nodes>") || xml.includes("<App ");
74
+ const escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
75
+ const sceneGraphContainsText = (xml, text) => xml.includes(escapeXmlAttribute(text));
73
76
  const assertSceneGraphNumberNear = (actual, expected, label, tolerance = 1) => {
74
77
  if (actual === void 0 || Math.abs(actual - expected) > tolerance) throw new Error(`expected ${label} ${expected}, got ${actual ?? "missing"}`);
75
78
  };
@@ -191,6 +194,17 @@ const pressKey = async (context, key) => {
191
194
  const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
192
195
  const queryMediaPlayerXml = async (context) => await queryEcp(context, "/query/media-player");
193
196
  const queryMediaPlayer = async (context) => readMediaPlayerInfo(await queryMediaPlayerXml(context));
197
+ const queryMediaPlayerXmlSafe = async (context) => {
198
+ try {
199
+ return await queryMediaPlayerXml(context);
200
+ } catch {
201
+ return;
202
+ }
203
+ };
204
+ const queryMediaPlayerSafe = async (context) => {
205
+ const xml = await queryMediaPlayerXmlSafe(context);
206
+ return xml === void 0 ? void 0 : readMediaPlayerInfo(xml);
207
+ };
194
208
  const readMediaPlayerInfo = (xml) => {
195
209
  const playerAttributes = /<player(?:\s+([^>]*))?>/.exec(xml)?.[1] ?? "";
196
210
  const formatAttributes = /<format(?:\s+([^>]*))?\/>/.exec(xml)?.[1] ?? "";
@@ -216,7 +230,12 @@ const readMediaPlayerInfo = (xml) => {
216
230
  const readMediaPlayerState = (xml) => readMediaPlayerInfo(xml).state;
217
231
  const readMediaPlayerPositionMs = (xml) => readMediaPlayerInfo(xml).positionMs;
218
232
  const readMediaPlayerContainer = (xml) => readMediaPlayerInfo(xml).container;
219
- const isActiveMediaPlayerState = (state) => state === "buffer" || state === "pause" || state === "play";
233
+ const isActiveMediaPlayerState = (state) => state === "buffer" || state === "buffering" || state === "pause" || state === "play";
234
+ const assertMediaPlayerContainer = async (context, expectedContainer) => {
235
+ const mediaPlayer = await queryMediaPlayer(context);
236
+ if (mediaPlayer.container !== expectedContainer) throw new Error(`expected media-player container ${expectedContainer}, got ${mediaPlayer.container ?? "unknown"}`);
237
+ return mediaPlayer;
238
+ };
220
239
  const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4) => {
221
240
  const start = Date.now();
222
241
  let lastState;
@@ -237,14 +256,22 @@ const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4)
237
256
  };
238
257
  const querySceneGraph = async (context, options = {}) => {
239
258
  const attempts = options.attempts ?? 1;
259
+ const requireComplete = options.requireComplete ?? false;
240
260
  const retryDelayMs = options.retryDelayMs ?? 500;
261
+ let lastXml = "";
241
262
  let lastError;
242
- for (let attempt = 0; attempt < attempts; attempt += 1) try {
243
- return await queryEcp(context, "/query/sgnodes/all");
244
- } catch (error) {
245
- lastError = error;
263
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
264
+ try {
265
+ const xml = await queryEcp(context, "/query/sgnodes/all");
266
+ if (!requireComplete || isCompleteSceneGraph(xml)) return xml;
267
+ lastXml = xml;
268
+ } catch (error) {
269
+ lastError = error;
270
+ lastXml = "";
271
+ }
246
272
  if (attempt < attempts - 1) await sleep(retryDelayMs);
247
273
  }
274
+ if (lastXml !== "") return lastXml;
248
275
  throw new Error(`SceneGraph query failed: ${formatErrorMessage(lastError)}`);
249
276
  };
250
277
  const assertSceneGraphNode = async (context, nodeName, expectation) => {
@@ -360,4 +387,4 @@ const sleep = (ms) => new Promise((resolve) => {
360
387
  });
361
388
  const formatErrorMessage = (error) => error instanceof Error ? error.message : String(error);
362
389
  //#endregion
363
- export { parseSceneGraphNumberList as A, readXmlTag as B, assertNamedNode as C, assertNamedNodeTranslation as D, assertNamedNodeText as E, readNamedNodeTranslation as F, readSceneGraphFailure as I, readSceneGraphStatus as L, readNamedNodeAttributes as M, readNamedNodeBounds as N, assertSceneGraphNumberNear as O, readNamedNodeNumber as P, readActiveApp as R, waitForSceneGraphNode as S, assertNamedNodeState as T, takeScreenshot as _, isActiveMediaPlayerState as a, waitForMediaPlayerState as b, queryActiveApp as c, queryMediaPlayerXml as d, querySceneGraph as f, readMediaPlayerState as g, readMediaPlayerPositionMs as h, installPackage as i, readNamedNodeAttribute as j, isNamedNodeVisible as k, queryEcp as l, readMediaPlayerInfo as m, checkDevice as n, launchApp as o, readMediaPlayerContainer as p, getDeviceInfo as r, pressKey as s, assertSceneGraphNode as t, queryMediaPlayer as u, validateRemoteKey as v, assertNamedNodeSize as w, waitForSceneGraphAssertion as x, waitForActiveApp as y, readXmlAttribute as z };
390
+ export { assertNamedNodeTranslation as A, readNamedNodeTranslation as B, waitForMediaPlayerState as C, assertNamedNodeSize as D, assertNamedNode as E, parseSceneGraphNumberList as F, readXmlAttribute as G, readSceneGraphStatus as H, readNamedNodeAttribute as I, readXmlTag as K, readNamedNodeAttributes as L, escapeXmlAttribute as M, isCompleteSceneGraph as N, assertNamedNodeState as O, isNamedNodeVisible as P, readNamedNodeBounds as R, waitForActiveApp as S, waitForSceneGraphNode as T, sceneGraphContainsText as U, readSceneGraphFailure as V, readActiveApp as W, readMediaPlayerInfo as _, installPackage as a, takeScreenshot as b, pressKey as c, queryMediaPlayer as d, queryMediaPlayerSafe as f, readMediaPlayerContainer as g, querySceneGraph as h, getDeviceInfo as i, assertSceneGraphNumberNear as j, assertNamedNodeText as k, queryActiveApp as l, queryMediaPlayerXmlSafe as m, assertSceneGraphNode as n, isActiveMediaPlayerState as o, queryMediaPlayerXml as p, checkDevice as r, launchApp as s, assertMediaPlayerContainer as t, queryEcp as u, readMediaPlayerPositionMs as v, waitForSceneGraphAssertion as w, validateRemoteKey as x, readMediaPlayerState as y, readNamedNodeNumber as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@putdotio/rokit",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "A tiny CLI companion for Roku device harness work.",
5
5
  "keywords": [
6
6
  "cli",