@putdotio/rokit 1.5.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 +19 -6
- package/dist/index.d.mts +34 -1
- package/dist/index.mjs +2 -2
- package/dist/rokit.mjs +40 -1
- package/dist/{roku-CNLr4tbj.mjs → roku-B61mpmWt.mjs} +96 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<p>
|
|
3
|
-
<img src="https://static.put.io/images/putio-boncuk.png" width="72">
|
|
4
|
-
</p>
|
|
5
|
-
|
|
6
2
|
<h1>rokit</h1>
|
|
7
3
|
|
|
8
4
|
<p>A tiny CLI companion for Roku device harness work.</p>
|
|
@@ -51,9 +47,11 @@ App-specific scenario scripts can also import the generic helpers:
|
|
|
51
47
|
import {
|
|
52
48
|
assertNamedNodeState,
|
|
53
49
|
assertNamedNodeTranslation,
|
|
50
|
+
assertMediaPlayerContainer,
|
|
54
51
|
assertSceneGraphNode,
|
|
55
52
|
pressKey,
|
|
56
53
|
querySceneGraph,
|
|
54
|
+
sceneGraphContainsText,
|
|
57
55
|
waitForSceneGraphAssertion,
|
|
58
56
|
type RokuContext,
|
|
59
57
|
} from "@putdotio/rokit";
|
|
@@ -71,11 +69,15 @@ const context: RokuContext = {
|
|
|
71
69
|
};
|
|
72
70
|
|
|
73
71
|
await pressKey(context, "Info");
|
|
74
|
-
const xml = await querySceneGraph(context, { attempts: 3 });
|
|
72
|
+
const xml = await querySceneGraph(context, { attempts: 3, requireComplete: true });
|
|
75
73
|
await assertSceneGraphNode(context, "videoPlayerScreen", { state: "visible" });
|
|
76
74
|
assertNamedNodeTranslation(xml, "videoPlayerScreen", 0, 0);
|
|
75
|
+
await assertMediaPlayerContainer(context, "mp4");
|
|
77
76
|
await waitForSceneGraphAssertion(context, "expected player", (xml) => {
|
|
78
77
|
assertNamedNodeState(xml, "videoPlayerScreen", "visible");
|
|
78
|
+
if (!sceneGraphContainsText(xml, "Ready")) {
|
|
79
|
+
throw new Error("expected ready text");
|
|
80
|
+
}
|
|
79
81
|
});
|
|
80
82
|
```
|
|
81
83
|
|
|
@@ -85,7 +87,9 @@ await waitForSceneGraphAssertion(context, "expected player", (xml) => {
|
|
|
85
87
|
rokit check
|
|
86
88
|
rokit device-info
|
|
87
89
|
rokit active-app
|
|
90
|
+
rokit media-player
|
|
88
91
|
rokit wait-active <app-id> [--timeout-ms <ms>]
|
|
92
|
+
rokit wait-media-player <state> [--timeout-ms <ms>]
|
|
89
93
|
rokit launch <app-id> [--param key=value]
|
|
90
94
|
rokit press [--delay-ms <ms>] <key> [key...]
|
|
91
95
|
rokit query <ecp-path>
|
|
@@ -111,8 +115,12 @@ and reports failures as `{ "status": "failed", "error": { "message": "..." } }`.
|
|
|
111
115
|
is reachable.
|
|
112
116
|
- `device-info` prints enhanced Roku device metadata as JSON.
|
|
113
117
|
- `active-app` prints the foreground app.
|
|
118
|
+
- `media-player` prints parsed `/query/media-player` playback state, including
|
|
119
|
+
state, container, position, duration, and format metadata.
|
|
114
120
|
- `wait-active` waits until the requested app is foregrounded and tolerates
|
|
115
121
|
transient ECP read failures while polling.
|
|
122
|
+
- `wait-media-player` waits until `/query/media-player` reports a target state
|
|
123
|
+
such as `play`, `pause`, or `buffer`.
|
|
116
124
|
- `launch` opens an app and waits until it is active. Use repeated `--param`
|
|
117
125
|
values for deeplink parameters. Roku launch responses can race app startup, so
|
|
118
126
|
launch accepts transient timeout/fetch failures and then verifies foreground
|
|
@@ -121,7 +129,9 @@ and reports failures as `{ "status": "failed", "error": { "message": "..." } }`.
|
|
|
121
129
|
sequences that need a stable gap between keys.
|
|
122
130
|
- `query` prints a raw ECP response such as `/query/sgnodes/all`.
|
|
123
131
|
- `sgnodes` prints the raw SceneGraph tree from `/query/sgnodes/all`. Library
|
|
124
|
-
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.
|
|
125
135
|
- `assert-node` checks a named SceneGraph node once.
|
|
126
136
|
- `wait-node` polls SceneGraph until a named node condition matches.
|
|
127
137
|
- `screenshot` saves a developer screenshot. It requires `ROKIT_PASSWORD`.
|
|
@@ -152,8 +162,11 @@ tokens, and app-specific media identifiers do not belong in git.
|
|
|
152
162
|
- launch and deeplink parameters
|
|
153
163
|
- remote keypresses
|
|
154
164
|
- raw ECP queries
|
|
165
|
+
- parsed media-player state from `/query/media-player`
|
|
166
|
+
- media-player active-state and container assertions
|
|
155
167
|
- SceneGraph state queries and named-node assertions
|
|
156
168
|
- SceneGraph attribute, numeric geometry, bounds, and translation readers
|
|
169
|
+
- SceneGraph completeness and escaped-text helpers
|
|
157
170
|
- SceneGraph geometry assertions, status/failure readers, and custom assertion
|
|
158
171
|
wait loops
|
|
159
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;
|
|
@@ -57,8 +60,27 @@ type DeviceSummary = {
|
|
|
57
60
|
readonly model: string;
|
|
58
61
|
readonly name: string;
|
|
59
62
|
};
|
|
63
|
+
type MediaPlayerState = "buffer" | "close" | "error" | "none" | "open" | "pause" | "play" | "stop" | string;
|
|
64
|
+
type MediaPlayerInfo = {
|
|
65
|
+
readonly audio?: string;
|
|
66
|
+
readonly buffering?: {
|
|
67
|
+
readonly current?: number;
|
|
68
|
+
readonly max?: number;
|
|
69
|
+
readonly target?: number;
|
|
70
|
+
};
|
|
71
|
+
readonly captions?: string;
|
|
72
|
+
readonly container?: string;
|
|
73
|
+
readonly durationMs?: number;
|
|
74
|
+
readonly error: boolean;
|
|
75
|
+
readonly isLive?: boolean;
|
|
76
|
+
readonly positionMs?: number;
|
|
77
|
+
readonly state?: MediaPlayerState;
|
|
78
|
+
readonly video?: string;
|
|
79
|
+
readonly videoResolution?: string;
|
|
80
|
+
};
|
|
60
81
|
type RetryOptions = {
|
|
61
82
|
readonly attempts?: number;
|
|
83
|
+
readonly requireComplete?: boolean;
|
|
62
84
|
readonly retryDelayMs?: number;
|
|
63
85
|
};
|
|
64
86
|
type SceneGraphAssertion = (xml: string) => void;
|
|
@@ -73,6 +95,17 @@ declare const waitForActiveApp: (context: RokuContext, appId: string, timeoutMs?
|
|
|
73
95
|
declare const launchApp: (context: RokuContext, appId: string, params?: ReadonlyMap<string, string>) => Promise<ActiveApp>;
|
|
74
96
|
declare const pressKey: (context: RokuContext, key: string) => Promise<void>;
|
|
75
97
|
declare const queryEcp: (context: RokuContext, path: string) => Promise<string>;
|
|
98
|
+
declare const queryMediaPlayerXml: (context: RokuContext) => Promise<string>;
|
|
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>;
|
|
102
|
+
declare const readMediaPlayerInfo: (xml: string) => MediaPlayerInfo;
|
|
103
|
+
declare const readMediaPlayerState: (xml: string) => MediaPlayerState | undefined;
|
|
104
|
+
declare const readMediaPlayerPositionMs: (xml: string) => number | undefined;
|
|
105
|
+
declare const readMediaPlayerContainer: (xml: string) => string | undefined;
|
|
106
|
+
declare const isActiveMediaPlayerState: (state: string | undefined) => boolean;
|
|
107
|
+
declare const assertMediaPlayerContainer: (context: RokuContext, expectedContainer: string) => Promise<MediaPlayerInfo>;
|
|
108
|
+
declare const waitForMediaPlayerState: (context: RokuContext, expectedState: MediaPlayerState, timeoutMs?: number) => Promise<MediaPlayerInfo>;
|
|
76
109
|
declare const querySceneGraph: (context: RokuContext, options?: RetryOptions) => Promise<string>;
|
|
77
110
|
declare const assertSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation) => Promise<void>;
|
|
78
111
|
declare const waitForSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation, timeoutMs?: number) => Promise<void>;
|
|
@@ -85,4 +118,4 @@ declare const takeScreenshot: (context: RokuContext & {
|
|
|
85
118
|
}, outputPath: string) => Promise<string>;
|
|
86
119
|
declare const validateRemoteKey: (key: string) => void;
|
|
87
120
|
//#endregion
|
|
88
|
-
export { type ActiveApp, type DeviceSummary, 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, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, 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
|
|
2
|
-
export { assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, 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 {
|
|
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";
|
|
@@ -98,6 +98,24 @@ const runCommand = async (context, command) => {
|
|
|
98
98
|
status: "ok"
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
+
if (command.name === "media-player") {
|
|
102
|
+
const mediaPlayer = await queryMediaPlayer(context);
|
|
103
|
+
return {
|
|
104
|
+
command: command.name,
|
|
105
|
+
data: mediaPlayer,
|
|
106
|
+
message: formatMediaPlayerMessage(mediaPlayer),
|
|
107
|
+
status: "ok"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (command.name === "wait-media-player") {
|
|
111
|
+
const mediaPlayer = await waitForMediaPlayerState(context, command.state, command.timeoutMs);
|
|
112
|
+
return {
|
|
113
|
+
command: command.name,
|
|
114
|
+
data: mediaPlayer,
|
|
115
|
+
message: formatMediaPlayerMessage(mediaPlayer),
|
|
116
|
+
status: "ok"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
101
119
|
if (command.name === "wait-active") {
|
|
102
120
|
const app = await waitForActiveApp(context, command.appId, command.timeoutMs);
|
|
103
121
|
return {
|
|
@@ -254,6 +272,16 @@ const parseCommand = (argv) => {
|
|
|
254
272
|
if (name === "check") return { name };
|
|
255
273
|
if (name === "device-info") return { name };
|
|
256
274
|
if (name === "active-app") return { name };
|
|
275
|
+
if (name === "media-player") return { name };
|
|
276
|
+
if (name === "wait-media-player") {
|
|
277
|
+
const state = args[0];
|
|
278
|
+
if (!state) fail("usage: rokit wait-media-player <state> [--timeout-ms <ms>]");
|
|
279
|
+
return {
|
|
280
|
+
name,
|
|
281
|
+
state,
|
|
282
|
+
timeoutMs: parseTimeoutOption(args.slice(1), "rokit wait-media-player <state>")
|
|
283
|
+
};
|
|
284
|
+
}
|
|
257
285
|
if (name === "wait-active") {
|
|
258
286
|
const appId = args[0];
|
|
259
287
|
if (!appId) fail("usage: rokit wait-active <app-id> [--timeout-ms <ms>]");
|
|
@@ -382,6 +410,15 @@ const formatNodeData = ({ expectation, nodeName, timeoutMs }) => ({
|
|
|
382
410
|
nodeName,
|
|
383
411
|
timeoutMs
|
|
384
412
|
});
|
|
413
|
+
const formatMediaPlayerMessage = (mediaPlayer) => {
|
|
414
|
+
return `media-player: ${[
|
|
415
|
+
`state=${mediaPlayer.state ?? "unknown"}`,
|
|
416
|
+
`container=${mediaPlayer.container ?? "unknown"}`,
|
|
417
|
+
`position=${formatMaybeMs(mediaPlayer.positionMs)}`,
|
|
418
|
+
`duration=${formatMaybeMs(mediaPlayer.durationMs)}`
|
|
419
|
+
].join(" ")}`;
|
|
420
|
+
};
|
|
421
|
+
const formatMaybeMs = (value) => value === void 0 ? "unknown" : `${value}ms`;
|
|
385
422
|
const sleep = (ms) => new Promise((resolve) => {
|
|
386
423
|
setTimeout(resolve, ms);
|
|
387
424
|
});
|
|
@@ -411,7 +448,9 @@ usage:
|
|
|
411
448
|
rokit check
|
|
412
449
|
rokit device-info
|
|
413
450
|
rokit active-app
|
|
451
|
+
rokit media-player
|
|
414
452
|
rokit wait-active <app-id> [--timeout-ms <ms>]
|
|
453
|
+
rokit wait-media-player <state> [--timeout-ms <ms>]
|
|
415
454
|
rokit launch <app-id> [--param key=value]
|
|
416
455
|
rokit press [--delay-ms <ms>] <key> [key...]
|
|
417
456
|
rokit query <ecp-path>
|
|
@@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
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
|
};
|
|
@@ -189,16 +192,86 @@ const pressKey = async (context, key) => {
|
|
|
189
192
|
await postOk(context, ecpUrl(context, `/keypress/${encodeURIComponent(key)}`));
|
|
190
193
|
};
|
|
191
194
|
const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
|
|
195
|
+
const queryMediaPlayerXml = async (context) => await queryEcp(context, "/query/media-player");
|
|
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
|
+
};
|
|
208
|
+
const readMediaPlayerInfo = (xml) => {
|
|
209
|
+
const playerAttributes = /<player(?:\s+([^>]*))?>/.exec(xml)?.[1] ?? "";
|
|
210
|
+
const formatAttributes = /<format(?:\s+([^>]*))?\/>/.exec(xml)?.[1] ?? "";
|
|
211
|
+
const bufferingAttributes = /<buffering(?:\s+([^>]*))?\/>/.exec(xml)?.[1];
|
|
212
|
+
return {
|
|
213
|
+
audio: readXmlAttribute(formatAttributes, "audio"),
|
|
214
|
+
buffering: bufferingAttributes === void 0 ? void 0 : {
|
|
215
|
+
current: readXmlNumberAttribute(bufferingAttributes, "current"),
|
|
216
|
+
max: readXmlNumberAttribute(bufferingAttributes, "max"),
|
|
217
|
+
target: readXmlNumberAttribute(bufferingAttributes, "target")
|
|
218
|
+
},
|
|
219
|
+
captions: readXmlAttribute(formatAttributes, "captions"),
|
|
220
|
+
container: readXmlAttribute(formatAttributes, "container"),
|
|
221
|
+
durationMs: readXmlNumberTag(xml, "duration"),
|
|
222
|
+
error: readXmlAttribute(playerAttributes, "error") === "true",
|
|
223
|
+
isLive: readXmlBooleanTag(xml, "is_live"),
|
|
224
|
+
positionMs: readXmlNumberTag(xml, "position"),
|
|
225
|
+
state: readXmlAttribute(playerAttributes, "state"),
|
|
226
|
+
video: readXmlAttribute(formatAttributes, "video"),
|
|
227
|
+
videoResolution: readXmlAttribute(formatAttributes, "video_res")
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
const readMediaPlayerState = (xml) => readMediaPlayerInfo(xml).state;
|
|
231
|
+
const readMediaPlayerPositionMs = (xml) => readMediaPlayerInfo(xml).positionMs;
|
|
232
|
+
const readMediaPlayerContainer = (xml) => readMediaPlayerInfo(xml).container;
|
|
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
|
+
};
|
|
239
|
+
const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4) => {
|
|
240
|
+
const start = Date.now();
|
|
241
|
+
let lastState;
|
|
242
|
+
let lastError;
|
|
243
|
+
while (Date.now() - start < timeoutMs) {
|
|
244
|
+
try {
|
|
245
|
+
const mediaPlayer = await queryMediaPlayer(context);
|
|
246
|
+
lastState = mediaPlayer.state;
|
|
247
|
+
lastError = void 0;
|
|
248
|
+
if (mediaPlayer.state === expectedState) return mediaPlayer;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
lastError = formatErrorMessage(error);
|
|
251
|
+
}
|
|
252
|
+
await sleep(500);
|
|
253
|
+
}
|
|
254
|
+
const suffix = lastError ? `; last ECP error: ${lastError}` : "";
|
|
255
|
+
throw new Error(`expected media-player state ${expectedState}, got ${lastState ?? "unknown"}${suffix}`);
|
|
256
|
+
};
|
|
192
257
|
const querySceneGraph = async (context, options = {}) => {
|
|
193
258
|
const attempts = options.attempts ?? 1;
|
|
259
|
+
const requireComplete = options.requireComplete ?? false;
|
|
194
260
|
const retryDelayMs = options.retryDelayMs ?? 500;
|
|
261
|
+
let lastXml = "";
|
|
195
262
|
let lastError;
|
|
196
|
-
for (let attempt = 0; attempt < attempts; attempt += 1)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
272
|
if (attempt < attempts - 1) await sleep(retryDelayMs);
|
|
201
273
|
}
|
|
274
|
+
if (lastXml !== "") return lastXml;
|
|
202
275
|
throw new Error(`SceneGraph query failed: ${formatErrorMessage(lastError)}`);
|
|
203
276
|
};
|
|
204
277
|
const assertSceneGraphNode = async (context, nodeName, expectation) => {
|
|
@@ -291,9 +364,27 @@ const postLaunchMaybeAccepted = async (context, url) => {
|
|
|
291
364
|
}
|
|
292
365
|
};
|
|
293
366
|
const ecpUrl = (context, path) => new URL(path, `http://${context.target}:${ecpPort}`);
|
|
367
|
+
const readXmlNumberTag = (xml, tag) => {
|
|
368
|
+
const rawValue = readXmlTag(xml, tag);
|
|
369
|
+
if (rawValue === void 0) return;
|
|
370
|
+
const match = /-?\d+(?:\.\d+)?/.exec(rawValue);
|
|
371
|
+
if (!match) return;
|
|
372
|
+
return Number(match[0]);
|
|
373
|
+
};
|
|
374
|
+
const readXmlBooleanTag = (xml, tag) => {
|
|
375
|
+
const value = readXmlTag(xml, tag);
|
|
376
|
+
if (value === "true") return true;
|
|
377
|
+
if (value === "false") return false;
|
|
378
|
+
};
|
|
379
|
+
const readXmlNumberAttribute = (attributes, name) => {
|
|
380
|
+
const value = readXmlAttribute(attributes, name);
|
|
381
|
+
if (value === void 0) return;
|
|
382
|
+
const parsed = Number(value);
|
|
383
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
384
|
+
};
|
|
294
385
|
const sleep = (ms) => new Promise((resolve) => {
|
|
295
386
|
setTimeout(resolve, ms);
|
|
296
387
|
});
|
|
297
388
|
const formatErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
298
389
|
//#endregion
|
|
299
|
-
export {
|
|
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 };
|