@putdotio/rokit 1.4.0 → 1.6.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 +16 -6
- package/dist/index.d.mts +42 -1
- package/dist/index.mjs +2 -2
- package/dist/rokit.mjs +40 -1
- package/dist/{roku-7hiM97p0.mjs → roku-CXHAzYR-.mjs} +103 -1
- 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>
|
|
@@ -49,10 +45,12 @@ App-specific scenario scripts can also import the generic helpers:
|
|
|
49
45
|
|
|
50
46
|
```ts
|
|
51
47
|
import {
|
|
48
|
+
assertNamedNodeState,
|
|
49
|
+
assertNamedNodeTranslation,
|
|
52
50
|
assertSceneGraphNode,
|
|
53
51
|
pressKey,
|
|
54
52
|
querySceneGraph,
|
|
55
|
-
|
|
53
|
+
waitForSceneGraphAssertion,
|
|
56
54
|
type RokuContext,
|
|
57
55
|
} from "@putdotio/rokit";
|
|
58
56
|
|
|
@@ -71,7 +69,10 @@ const context: RokuContext = {
|
|
|
71
69
|
await pressKey(context, "Info");
|
|
72
70
|
const xml = await querySceneGraph(context, { attempts: 3 });
|
|
73
71
|
await assertSceneGraphNode(context, "videoPlayerScreen", { state: "visible" });
|
|
74
|
-
|
|
72
|
+
assertNamedNodeTranslation(xml, "videoPlayerScreen", 0, 0);
|
|
73
|
+
await waitForSceneGraphAssertion(context, "expected player", (xml) => {
|
|
74
|
+
assertNamedNodeState(xml, "videoPlayerScreen", "visible");
|
|
75
|
+
});
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
## Commands
|
|
@@ -80,7 +81,9 @@ console.log(readNamedNodeTranslation(xml, "videoPlayerScreen"));
|
|
|
80
81
|
rokit check
|
|
81
82
|
rokit device-info
|
|
82
83
|
rokit active-app
|
|
84
|
+
rokit media-player
|
|
83
85
|
rokit wait-active <app-id> [--timeout-ms <ms>]
|
|
86
|
+
rokit wait-media-player <state> [--timeout-ms <ms>]
|
|
84
87
|
rokit launch <app-id> [--param key=value]
|
|
85
88
|
rokit press [--delay-ms <ms>] <key> [key...]
|
|
86
89
|
rokit query <ecp-path>
|
|
@@ -106,8 +109,12 @@ and reports failures as `{ "status": "failed", "error": { "message": "..." } }`.
|
|
|
106
109
|
is reachable.
|
|
107
110
|
- `device-info` prints enhanced Roku device metadata as JSON.
|
|
108
111
|
- `active-app` prints the foreground app.
|
|
112
|
+
- `media-player` prints parsed `/query/media-player` playback state, including
|
|
113
|
+
state, container, position, duration, and format metadata.
|
|
109
114
|
- `wait-active` waits until the requested app is foregrounded and tolerates
|
|
110
115
|
transient ECP read failures while polling.
|
|
116
|
+
- `wait-media-player` waits until `/query/media-player` reports a target state
|
|
117
|
+
such as `play`, `pause`, or `buffer`.
|
|
111
118
|
- `launch` opens an app and waits until it is active. Use repeated `--param`
|
|
112
119
|
values for deeplink parameters. Roku launch responses can race app startup, so
|
|
113
120
|
launch accepts transient timeout/fetch failures and then verifies foreground
|
|
@@ -147,8 +154,11 @@ tokens, and app-specific media identifiers do not belong in git.
|
|
|
147
154
|
- launch and deeplink parameters
|
|
148
155
|
- remote keypresses
|
|
149
156
|
- raw ECP queries
|
|
157
|
+
- parsed media-player state from `/query/media-player`
|
|
150
158
|
- SceneGraph state queries and named-node assertions
|
|
151
159
|
- SceneGraph attribute, numeric geometry, bounds, and translation readers
|
|
160
|
+
- SceneGraph geometry assertions, status/failure readers, and custom assertion
|
|
161
|
+
wait loops
|
|
152
162
|
- screenshots
|
|
153
163
|
|
|
154
164
|
App repositories should keep their own scenario commands for product behavior,
|
package/dist/index.d.mts
CHANGED
|
@@ -4,6 +4,10 @@ import * as rokuDeploy from "roku-deploy";
|
|
|
4
4
|
type NodeState = "absent" | "hidden" | "visible";
|
|
5
5
|
type SceneGraphBounds = readonly [x: number, y: number, width: number, height: number];
|
|
6
6
|
type SceneGraphPoint = readonly [x: number, y: number];
|
|
7
|
+
type SceneGraphStatus = {
|
|
8
|
+
readonly error?: string;
|
|
9
|
+
readonly status?: string;
|
|
10
|
+
};
|
|
7
11
|
type NodeExpectation = {
|
|
8
12
|
readonly state: NodeState;
|
|
9
13
|
readonly text?: string;
|
|
@@ -16,7 +20,12 @@ declare const readNamedNodeAttribute: (xml: string, nodeName: string, attributeN
|
|
|
16
20
|
declare const readNamedNodeNumber: (xml: string, nodeName: string, attributeName: string) => number | undefined;
|
|
17
21
|
declare const readNamedNodeBounds: (xml: string, nodeName: string) => SceneGraphBounds | undefined;
|
|
18
22
|
declare const readNamedNodeTranslation: (xml: string, nodeName: string) => SceneGraphPoint | undefined;
|
|
23
|
+
declare const readSceneGraphStatus: (xml: string) => SceneGraphStatus;
|
|
24
|
+
declare const readSceneGraphFailure: (xml: string) => string | undefined;
|
|
19
25
|
declare const isNamedNodeVisible: (xml: string, nodeName: string) => boolean;
|
|
26
|
+
declare const assertSceneGraphNumberNear: (actual: number | undefined, expected: number, label: string, tolerance?: number) => void;
|
|
27
|
+
declare const assertNamedNodeTranslation: (xml: string, nodeName: string, expectedX: number, expectedY: number, tolerance?: number) => void;
|
|
28
|
+
declare const assertNamedNodeSize: (xml: string, nodeName: string, expectedWidth: number, expectedHeight: number, tolerance?: number) => void;
|
|
20
29
|
declare const assertNamedNode: (xml: string, nodeName: string, expectation: NodeExpectation) => void;
|
|
21
30
|
declare const assertNamedNodeState: (xml: string, nodeName: string, state: NodeState) => void;
|
|
22
31
|
declare const assertNamedNodeText: (xml: string, nodeName: string, expectedText: string) => void;
|
|
@@ -48,10 +57,33 @@ type DeviceSummary = {
|
|
|
48
57
|
readonly model: string;
|
|
49
58
|
readonly name: string;
|
|
50
59
|
};
|
|
60
|
+
type MediaPlayerState = "buffer" | "close" | "error" | "none" | "open" | "pause" | "play" | "stop" | string;
|
|
61
|
+
type MediaPlayerInfo = {
|
|
62
|
+
readonly audio?: string;
|
|
63
|
+
readonly buffering?: {
|
|
64
|
+
readonly current?: number;
|
|
65
|
+
readonly max?: number;
|
|
66
|
+
readonly target?: number;
|
|
67
|
+
};
|
|
68
|
+
readonly captions?: string;
|
|
69
|
+
readonly container?: string;
|
|
70
|
+
readonly durationMs?: number;
|
|
71
|
+
readonly error: boolean;
|
|
72
|
+
readonly isLive?: boolean;
|
|
73
|
+
readonly positionMs?: number;
|
|
74
|
+
readonly state?: MediaPlayerState;
|
|
75
|
+
readonly video?: string;
|
|
76
|
+
readonly videoResolution?: string;
|
|
77
|
+
};
|
|
51
78
|
type RetryOptions = {
|
|
52
79
|
readonly attempts?: number;
|
|
53
80
|
readonly retryDelayMs?: number;
|
|
54
81
|
};
|
|
82
|
+
type SceneGraphAssertion = (xml: string) => void;
|
|
83
|
+
type WaitForSceneGraphAssertionOptions = {
|
|
84
|
+
readonly pollIntervalMs?: number;
|
|
85
|
+
readonly timeoutMs?: number;
|
|
86
|
+
};
|
|
55
87
|
declare const checkDevice: (context: RokuContext) => Promise<DeviceSummary>;
|
|
56
88
|
declare const getDeviceInfo: (context: RokuContext) => Promise<rokuDeploy.DeviceInfo>;
|
|
57
89
|
declare const queryActiveApp: (context: RokuContext) => Promise<ActiveApp>;
|
|
@@ -59,9 +91,18 @@ declare const waitForActiveApp: (context: RokuContext, appId: string, timeoutMs?
|
|
|
59
91
|
declare const launchApp: (context: RokuContext, appId: string, params?: ReadonlyMap<string, string>) => Promise<ActiveApp>;
|
|
60
92
|
declare const pressKey: (context: RokuContext, key: string) => Promise<void>;
|
|
61
93
|
declare const queryEcp: (context: RokuContext, path: string) => Promise<string>;
|
|
94
|
+
declare const queryMediaPlayerXml: (context: RokuContext) => Promise<string>;
|
|
95
|
+
declare const queryMediaPlayer: (context: RokuContext) => Promise<MediaPlayerInfo>;
|
|
96
|
+
declare const readMediaPlayerInfo: (xml: string) => MediaPlayerInfo;
|
|
97
|
+
declare const readMediaPlayerState: (xml: string) => MediaPlayerState | undefined;
|
|
98
|
+
declare const readMediaPlayerPositionMs: (xml: string) => number | undefined;
|
|
99
|
+
declare const readMediaPlayerContainer: (xml: string) => string | undefined;
|
|
100
|
+
declare const isActiveMediaPlayerState: (state: string | undefined) => boolean;
|
|
101
|
+
declare const waitForMediaPlayerState: (context: RokuContext, expectedState: MediaPlayerState, timeoutMs?: number) => Promise<MediaPlayerInfo>;
|
|
62
102
|
declare const querySceneGraph: (context: RokuContext, options?: RetryOptions) => Promise<string>;
|
|
63
103
|
declare const assertSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation) => Promise<void>;
|
|
64
104
|
declare const waitForSceneGraphNode: (context: RokuContext, nodeName: string, expectation: NodeExpectation, timeoutMs?: number) => Promise<void>;
|
|
105
|
+
declare const waitForSceneGraphAssertion: (context: RokuContext, description: string, assertXml: SceneGraphAssertion, options?: WaitForSceneGraphAssertionOptions) => Promise<string>;
|
|
65
106
|
declare const installPackage: (context: RokuContext & {
|
|
66
107
|
readonly password: string;
|
|
67
108
|
}, zipPath: string) => Promise<string>;
|
|
@@ -70,4 +111,4 @@ declare const takeScreenshot: (context: RokuContext & {
|
|
|
70
111
|
}, outputPath: string) => Promise<string>;
|
|
71
112
|
declare const validateRemoteKey: (key: string) => void;
|
|
72
113
|
//#endregion
|
|
73
|
-
export { type ActiveApp, type DeviceSummary, type NodeExpectation, type NodeState, type RemoteKey, type RetryOptions, type RokuContext, type SceneGraphBounds, type SceneGraphPoint, assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
|
|
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { assertNamedNode, assertNamedNodeState, assertNamedNodeText, assertSceneGraphNode, checkDevice, getDeviceInfo, installPackage, isNamedNodeVisible, launchApp, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, querySceneGraph, readActiveApp, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readXmlAttribute, readXmlTag, takeScreenshot, validateRemoteKey, waitForActiveApp, waitForSceneGraphNode };
|
|
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 };
|
package/dist/rokit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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";
|
|
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>
|
|
@@ -58,10 +58,30 @@ const readNamedNodeTranslation = (xml, nodeName) => {
|
|
|
58
58
|
if (parts.length < 2) return;
|
|
59
59
|
return [parts[0], parts[1]];
|
|
60
60
|
};
|
|
61
|
+
const readSceneGraphStatus = (xml) => ({
|
|
62
|
+
error: readXmlTag(xml, "error"),
|
|
63
|
+
status: readXmlTag(xml, "status")
|
|
64
|
+
});
|
|
65
|
+
const readSceneGraphFailure = (xml) => {
|
|
66
|
+
const { error, status } = readSceneGraphStatus(xml);
|
|
67
|
+
return status === "FAILED" ? error ?? "unknown" : void 0;
|
|
68
|
+
};
|
|
61
69
|
const isNamedNodeVisible = (xml, nodeName) => {
|
|
62
70
|
const attributes = readNamedNodeAttributes(xml, nodeName);
|
|
63
71
|
return attributes !== void 0 && !attributes.includes("visible=\"false\"");
|
|
64
72
|
};
|
|
73
|
+
const assertSceneGraphNumberNear = (actual, expected, label, tolerance = 1) => {
|
|
74
|
+
if (actual === void 0 || Math.abs(actual - expected) > tolerance) throw new Error(`expected ${label} ${expected}, got ${actual ?? "missing"}`);
|
|
75
|
+
};
|
|
76
|
+
const assertNamedNodeTranslation = (xml, nodeName, expectedX, expectedY, tolerance = 1) => {
|
|
77
|
+
const translation = readNamedNodeTranslation(xml, nodeName);
|
|
78
|
+
assertSceneGraphNumberNear(translation?.[0], expectedX, `${nodeName} x`, tolerance);
|
|
79
|
+
assertSceneGraphNumberNear(translation?.[1], expectedY, `${nodeName} y`, tolerance);
|
|
80
|
+
};
|
|
81
|
+
const assertNamedNodeSize = (xml, nodeName, expectedWidth, expectedHeight, tolerance = 1) => {
|
|
82
|
+
assertSceneGraphNumberNear(readNamedNodeNumber(xml, nodeName, "width"), expectedWidth, `${nodeName} width`, tolerance);
|
|
83
|
+
assertSceneGraphNumberNear(readNamedNodeNumber(xml, nodeName, "height"), expectedHeight, `${nodeName} height`, tolerance);
|
|
84
|
+
};
|
|
65
85
|
const assertNamedNode = (xml, nodeName, expectation) => {
|
|
66
86
|
if ("attribute" in expectation) {
|
|
67
87
|
assertNamedNodeAttribute(xml, nodeName, expectation.attribute, expectation.value);
|
|
@@ -169,6 +189,52 @@ const pressKey = async (context, key) => {
|
|
|
169
189
|
await postOk(context, ecpUrl(context, `/keypress/${encodeURIComponent(key)}`));
|
|
170
190
|
};
|
|
171
191
|
const queryEcp = async (context, path) => await fetchText(context, path.startsWith("/") ? path : `/${path}`);
|
|
192
|
+
const queryMediaPlayerXml = async (context) => await queryEcp(context, "/query/media-player");
|
|
193
|
+
const queryMediaPlayer = async (context) => readMediaPlayerInfo(await queryMediaPlayerXml(context));
|
|
194
|
+
const readMediaPlayerInfo = (xml) => {
|
|
195
|
+
const playerAttributes = /<player(?:\s+([^>]*))?>/.exec(xml)?.[1] ?? "";
|
|
196
|
+
const formatAttributes = /<format(?:\s+([^>]*))?\/>/.exec(xml)?.[1] ?? "";
|
|
197
|
+
const bufferingAttributes = /<buffering(?:\s+([^>]*))?\/>/.exec(xml)?.[1];
|
|
198
|
+
return {
|
|
199
|
+
audio: readXmlAttribute(formatAttributes, "audio"),
|
|
200
|
+
buffering: bufferingAttributes === void 0 ? void 0 : {
|
|
201
|
+
current: readXmlNumberAttribute(bufferingAttributes, "current"),
|
|
202
|
+
max: readXmlNumberAttribute(bufferingAttributes, "max"),
|
|
203
|
+
target: readXmlNumberAttribute(bufferingAttributes, "target")
|
|
204
|
+
},
|
|
205
|
+
captions: readXmlAttribute(formatAttributes, "captions"),
|
|
206
|
+
container: readXmlAttribute(formatAttributes, "container"),
|
|
207
|
+
durationMs: readXmlNumberTag(xml, "duration"),
|
|
208
|
+
error: readXmlAttribute(playerAttributes, "error") === "true",
|
|
209
|
+
isLive: readXmlBooleanTag(xml, "is_live"),
|
|
210
|
+
positionMs: readXmlNumberTag(xml, "position"),
|
|
211
|
+
state: readXmlAttribute(playerAttributes, "state"),
|
|
212
|
+
video: readXmlAttribute(formatAttributes, "video"),
|
|
213
|
+
videoResolution: readXmlAttribute(formatAttributes, "video_res")
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const readMediaPlayerState = (xml) => readMediaPlayerInfo(xml).state;
|
|
217
|
+
const readMediaPlayerPositionMs = (xml) => readMediaPlayerInfo(xml).positionMs;
|
|
218
|
+
const readMediaPlayerContainer = (xml) => readMediaPlayerInfo(xml).container;
|
|
219
|
+
const isActiveMediaPlayerState = (state) => state === "buffer" || state === "pause" || state === "play";
|
|
220
|
+
const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4) => {
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
let lastState;
|
|
223
|
+
let lastError;
|
|
224
|
+
while (Date.now() - start < timeoutMs) {
|
|
225
|
+
try {
|
|
226
|
+
const mediaPlayer = await queryMediaPlayer(context);
|
|
227
|
+
lastState = mediaPlayer.state;
|
|
228
|
+
lastError = void 0;
|
|
229
|
+
if (mediaPlayer.state === expectedState) return mediaPlayer;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
lastError = formatErrorMessage(error);
|
|
232
|
+
}
|
|
233
|
+
await sleep(500);
|
|
234
|
+
}
|
|
235
|
+
const suffix = lastError ? `; last ECP error: ${lastError}` : "";
|
|
236
|
+
throw new Error(`expected media-player state ${expectedState}, got ${lastState ?? "unknown"}${suffix}`);
|
|
237
|
+
};
|
|
172
238
|
const querySceneGraph = async (context, options = {}) => {
|
|
173
239
|
const attempts = options.attempts ?? 1;
|
|
174
240
|
const retryDelayMs = options.retryDelayMs ?? 500;
|
|
@@ -199,6 +265,24 @@ const waitForSceneGraphNode = async (context, nodeName, expectation, timeoutMs =
|
|
|
199
265
|
const suffix = lastError ? `; last observation: ${lastError}` : "";
|
|
200
266
|
throw new Error(`expected SceneGraph node "${nodeName}" to match condition${suffix}`);
|
|
201
267
|
};
|
|
268
|
+
const waitForSceneGraphAssertion = async (context, description, assertXml, options = {}) => {
|
|
269
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
270
|
+
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
271
|
+
const start = Date.now();
|
|
272
|
+
let lastError;
|
|
273
|
+
while (Date.now() - start < timeoutMs) {
|
|
274
|
+
try {
|
|
275
|
+
const xml = await querySceneGraph(context);
|
|
276
|
+
assertXml(xml);
|
|
277
|
+
return xml;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
lastError = formatErrorMessage(error);
|
|
280
|
+
}
|
|
281
|
+
await sleep(pollIntervalMs);
|
|
282
|
+
}
|
|
283
|
+
const suffix = lastError ? `; last observation: ${lastError}` : "";
|
|
284
|
+
throw new Error(`${description}${suffix}`);
|
|
285
|
+
};
|
|
202
286
|
const installPackage = async (context, zipPath) => {
|
|
203
287
|
const resolvedZip = resolve(zipPath);
|
|
204
288
|
const extension = extname(resolvedZip);
|
|
@@ -253,9 +337,27 @@ const postLaunchMaybeAccepted = async (context, url) => {
|
|
|
253
337
|
}
|
|
254
338
|
};
|
|
255
339
|
const ecpUrl = (context, path) => new URL(path, `http://${context.target}:${ecpPort}`);
|
|
340
|
+
const readXmlNumberTag = (xml, tag) => {
|
|
341
|
+
const rawValue = readXmlTag(xml, tag);
|
|
342
|
+
if (rawValue === void 0) return;
|
|
343
|
+
const match = /-?\d+(?:\.\d+)?/.exec(rawValue);
|
|
344
|
+
if (!match) return;
|
|
345
|
+
return Number(match[0]);
|
|
346
|
+
};
|
|
347
|
+
const readXmlBooleanTag = (xml, tag) => {
|
|
348
|
+
const value = readXmlTag(xml, tag);
|
|
349
|
+
if (value === "true") return true;
|
|
350
|
+
if (value === "false") return false;
|
|
351
|
+
};
|
|
352
|
+
const readXmlNumberAttribute = (attributes, name) => {
|
|
353
|
+
const value = readXmlAttribute(attributes, name);
|
|
354
|
+
if (value === void 0) return;
|
|
355
|
+
const parsed = Number(value);
|
|
356
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
357
|
+
};
|
|
256
358
|
const sleep = (ms) => new Promise((resolve) => {
|
|
257
359
|
setTimeout(resolve, ms);
|
|
258
360
|
});
|
|
259
361
|
const formatErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
260
362
|
//#endregion
|
|
261
|
-
export {
|
|
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 };
|