@putdotio/rokit 2.1.0 → 2.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 +7 -1
- package/dist/{debug-af6vvS9T.mjs → debug-BbJdwNkW.mjs} +61 -3
- package/dist/index.d.mts +10 -1
- package/dist/index.mjs +2 -2
- package/dist/rokit.mjs +3 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ App-specific scenario scripts can import the generic helpers:
|
|
|
106
106
|
import {
|
|
107
107
|
assertMediaPlayerContainer,
|
|
108
108
|
assertSceneGraphNode,
|
|
109
|
+
captureScreenshot,
|
|
109
110
|
pressKey,
|
|
110
111
|
querySceneGraph,
|
|
111
112
|
waitForSceneGraphAssertion,
|
|
@@ -124,9 +125,14 @@ const context: RokuContext = {
|
|
|
124
125
|
};
|
|
125
126
|
|
|
126
127
|
await pressKey(context, "Info");
|
|
127
|
-
await querySceneGraph(context, { attempts: 3, requireComplete: true });
|
|
128
|
+
await querySceneGraph(context, { attempts: 3, requireAppNode: true, requireComplete: true });
|
|
128
129
|
await assertSceneGraphNode(context, "videoPlayerScreen", { state: "visible" });
|
|
129
130
|
await assertMediaPlayerContainer(context, "mp4");
|
|
131
|
+
await captureScreenshot(
|
|
132
|
+
{ ...context, password: process.env.ROKIT_PASSWORD ?? "" },
|
|
133
|
+
"artifacts/player.jpg",
|
|
134
|
+
{ attempts: 3 },
|
|
135
|
+
);
|
|
130
136
|
await waitForSceneGraphAssertion(context, "player ready", (xml) => {
|
|
131
137
|
if (!xml.includes("videoPlayerScreen")) {
|
|
132
138
|
throw new Error("expected player screen");
|
|
@@ -4,6 +4,8 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import * as rokuDeploy from "roku-deploy";
|
|
6
6
|
import { createSocket } from "node:dgram";
|
|
7
|
+
import { access, copyFile, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
7
9
|
//#region src/errors.ts
|
|
8
10
|
var InvalidInput = class extends Schema.TaggedErrorClass()("InvalidInput", { message: Schema.String }) {};
|
|
9
11
|
var MissingTarget = class extends Schema.TaggedErrorClass()("MissingTarget", {}) {
|
|
@@ -335,22 +337,31 @@ const waitForMediaPlayerState = async (context, expectedState, timeoutMs = 1e4)
|
|
|
335
337
|
};
|
|
336
338
|
const querySceneGraph = async (context, options = {}) => {
|
|
337
339
|
const attempts = options.attempts ?? 1;
|
|
340
|
+
const requireAppNode = options.requireAppNode ?? false;
|
|
338
341
|
const requireComplete = options.requireComplete ?? false;
|
|
339
342
|
const retryDelayMs = options.retryDelayMs ?? 500;
|
|
340
343
|
let lastXml = "";
|
|
341
344
|
let lastError;
|
|
345
|
+
let lastMissingAppNode = false;
|
|
342
346
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
343
347
|
try {
|
|
344
348
|
const xml = await queryEcp(context, "/query/sgnodes/all");
|
|
345
|
-
|
|
349
|
+
const complete = !requireComplete || isCompleteSceneGraph(xml);
|
|
350
|
+
const hasAppNode = !requireAppNode || !xml.includes("<All_Nodes>") || xml.includes("<App ");
|
|
351
|
+
if (complete && hasAppNode) return xml;
|
|
346
352
|
lastXml = xml;
|
|
353
|
+
lastMissingAppNode = complete && !hasAppNode;
|
|
347
354
|
} catch (error) {
|
|
348
355
|
lastError = error;
|
|
349
356
|
lastXml = "";
|
|
357
|
+
lastMissingAppNode = false;
|
|
350
358
|
}
|
|
351
359
|
if (attempt < attempts - 1) await sleep(retryDelayMs);
|
|
352
360
|
}
|
|
353
|
-
if (lastXml !== "")
|
|
361
|
+
if (lastXml !== "") {
|
|
362
|
+
if (lastMissingAppNode) throw new Error("SceneGraph query returned complete XML without an App node");
|
|
363
|
+
throw new Error("SceneGraph query returned incomplete XML");
|
|
364
|
+
}
|
|
354
365
|
throw new Error(`SceneGraph query failed: ${formatErrorMessage$1(lastError)}`);
|
|
355
366
|
};
|
|
356
367
|
const assertSceneGraphNode = async (context, nodeName, expectation) => {
|
|
@@ -411,6 +422,41 @@ const takeScreenshot = async (context, outputPath) => {
|
|
|
411
422
|
password: context.password
|
|
412
423
|
});
|
|
413
424
|
};
|
|
425
|
+
const captureScreenshot = async (context, outputPath, options = {}) => {
|
|
426
|
+
const attempts = options.attempts ?? 3;
|
|
427
|
+
const retryDelayMs = options.retryDelayMs ?? 1500;
|
|
428
|
+
const resolvedOutput = resolve(outputPath);
|
|
429
|
+
await mkdir(dirname(resolvedOutput), { recursive: true });
|
|
430
|
+
let lastError = "unknown";
|
|
431
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
432
|
+
const captureDir = await mkdtemp(join(tmpdir(), `${safeTempPrefix(options.tempDirPrefix ?? "rokit-screenshot")}-`));
|
|
433
|
+
const capturePath = join(captureDir, basename(resolvedOutput));
|
|
434
|
+
try {
|
|
435
|
+
const tempResult = await firstExistingPath([await takeScreenshot(context, capturePath), capturePath]);
|
|
436
|
+
if (tempResult !== void 0) {
|
|
437
|
+
await copyFile(tempResult, resolvedOutput);
|
|
438
|
+
return resolvedOutput;
|
|
439
|
+
}
|
|
440
|
+
const directResult = await firstExistingPath([resolvedOutput, await takeScreenshot(context, resolvedOutput)]);
|
|
441
|
+
if (directResult === resolvedOutput) return resolvedOutput;
|
|
442
|
+
if (directResult !== void 0) {
|
|
443
|
+
await copyFile(directResult, resolvedOutput);
|
|
444
|
+
return resolvedOutput;
|
|
445
|
+
}
|
|
446
|
+
throw new Error("screenshot capture succeeded without writing an image file");
|
|
447
|
+
} catch (error) {
|
|
448
|
+
lastError = formatErrorMessage$1(error);
|
|
449
|
+
if (attempt === attempts) break;
|
|
450
|
+
await sleep(retryDelayMs);
|
|
451
|
+
} finally {
|
|
452
|
+
await rm(captureDir, {
|
|
453
|
+
force: true,
|
|
454
|
+
recursive: true
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`failed to capture ${basename(resolvedOutput)}: ${lastError}`);
|
|
459
|
+
};
|
|
414
460
|
const packageChannel = async (outputPath, rootDir = process.cwd()) => {
|
|
415
461
|
const options = packageOptions(outputPath, rootDir);
|
|
416
462
|
await rokuDeploy.createPackage(options);
|
|
@@ -444,6 +490,18 @@ const rejectUnsafeInput$1 = (value, label) => {
|
|
|
444
490
|
return code < 32 || code === 127;
|
|
445
491
|
})) throw new Error(`${label} contains control characters`);
|
|
446
492
|
};
|
|
493
|
+
const safeTempPrefix = (value) => value.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
494
|
+
const firstExistingPath = async (paths) => {
|
|
495
|
+
for (const path of paths) if (await fileExists(path)) return path;
|
|
496
|
+
};
|
|
497
|
+
const fileExists = async (path) => {
|
|
498
|
+
try {
|
|
499
|
+
await access(path);
|
|
500
|
+
return true;
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
447
505
|
const validateRemoteKey = (key) => {
|
|
448
506
|
if (key.startsWith("Lit_")) return;
|
|
449
507
|
if (!remoteKeySet.has(key)) throw new Error(`unsupported remote key: ${key}`);
|
|
@@ -747,4 +805,4 @@ const validateNonNegativeInteger = (value, label) => {
|
|
|
747
805
|
if (!Number.isInteger(parsed) || parsed < 0) fail(`Invalid ${label}: ${value}`);
|
|
748
806
|
};
|
|
749
807
|
//#endregion
|
|
750
|
-
export {
|
|
808
|
+
export { readNamedNodeAttributes as $, readMediaPlayerContainer as A, waitForSceneGraphAssertion as B, queryActiveApp as C, queryMediaPlayerXml as D, queryMediaPlayerSafe as E, takeScreenshot as F, assertNamedNodeText as G, assertNamedNode as H, validateEcpPath as I, escapeXmlAttribute as J, assertNamedNodeTranslation as K, validateRemoteKey as L, readMediaPlayerPositionMs as M, readMediaPlayerState as N, queryMediaPlayerXmlSafe as O, resolvePackageOutputPath as P, readNamedNodeAttribute as Q, waitForActiveApp as R, pressKey as S, queryMediaPlayer as T, assertNamedNodeSize as U, waitForSceneGraphNode as V, assertNamedNodeState as W, isNamedNodeVisible as X, isCompleteSceneGraph as Y, parseSceneGraphNumberList as Z, getDeviceInfo as _, loadEnv as a, sceneGraphContainsText as at, launchApp as b, requirePassword as c, readXmlTag as ct, resolveOutputPath as d, readNamedNodeBounds as et, assertMediaPlayerContainer as f, discoverRokuDevices as g, checkDevice as h, fail as i, readSceneGraphStatus as it, readMediaPlayerInfo as j, querySceneGraph as k, requireTarget as l, normalizeError as lt, captureScreenshot as m, captureDebugConsole as n, readNamedNodeTranslation as nt, loadLocalEnv as o, readActiveApp as ot, assertSceneGraphNode as p, assertSceneGraphNumberNear as q, runDebugCommand as r, readSceneGraphFailure as rt, rejectUnsafeEcpPath as s, readXmlAttribute as st, buildDebugCommand as t, readNamedNodeNumber as tt, resolveFileOutputPath as u, renderError as ut, installPackage as v, queryEcp as w, packageChannel as x, isActiveMediaPlayerState as y, waitForMediaPlayerState as z };
|
package/dist/index.d.mts
CHANGED
|
@@ -88,6 +88,7 @@ type MediaPlayerInfo = {
|
|
|
88
88
|
};
|
|
89
89
|
type RetryOptions = {
|
|
90
90
|
readonly attempts?: number;
|
|
91
|
+
readonly requireAppNode?: boolean;
|
|
91
92
|
readonly requireComplete?: boolean;
|
|
92
93
|
readonly retryDelayMs?: number;
|
|
93
94
|
};
|
|
@@ -99,6 +100,11 @@ type WaitForSceneGraphAssertionOptions = {
|
|
|
99
100
|
type PackageResult = {
|
|
100
101
|
readonly path: string;
|
|
101
102
|
};
|
|
103
|
+
type ScreenshotCaptureOptions = {
|
|
104
|
+
readonly attempts?: number;
|
|
105
|
+
readonly retryDelayMs?: number;
|
|
106
|
+
readonly tempDirPrefix?: string;
|
|
107
|
+
};
|
|
102
108
|
declare const checkDevice: (context: RokuContext) => Promise<DeviceSummary>;
|
|
103
109
|
declare const getDeviceInfo: (context: RokuContext) => Promise<rokuDeploy.DeviceInfo>;
|
|
104
110
|
declare const queryActiveApp: (context: RokuContext) => Promise<ActiveApp>;
|
|
@@ -128,6 +134,9 @@ declare const installPackage: (context: RokuContext & {
|
|
|
128
134
|
declare const takeScreenshot: (context: RokuContext & {
|
|
129
135
|
readonly password: string;
|
|
130
136
|
}, outputPath: string) => Promise<string>;
|
|
137
|
+
declare const captureScreenshot: (context: RokuContext & {
|
|
138
|
+
readonly password: string;
|
|
139
|
+
}, outputPath: string, options?: ScreenshotCaptureOptions) => Promise<string>;
|
|
131
140
|
declare const packageChannel: (outputPath: string, rootDir?: string) => Promise<PackageResult>;
|
|
132
141
|
declare const validateEcpPath: (path: string) => string;
|
|
133
142
|
declare const validateRemoteKey: (key: string) => void;
|
|
@@ -159,4 +168,4 @@ declare const buildDebugCommand: (command: string, args: readonly string[]) => R
|
|
|
159
168
|
declare const runDebugCommand: (context: RokuContext, command: RokuDebugCommand, durationMs: number, idleTimeoutMs: number) => Promise<DebugCommandResult>;
|
|
160
169
|
declare const captureDebugConsole: (context: RokuContext, durationMs: number) => Promise<DebugConsoleCapture>;
|
|
161
170
|
//#endregion
|
|
162
|
-
export { type ActiveApp, type DebugCommandResult, type DebugConsoleCapture, type DeviceSummary, type DiscoveredRokuDevice, type MediaPlayerInfo, type MediaPlayerState, type NodeExpectation, type NodeState, type PackageResult, type RemoteKey, type RetryOptions, type RokuContext, type RokuDebugCommand, type RokuDebugPort, type SceneGraphAssertion, type SceneGraphBounds, type SceneGraphPoint, type SceneGraphStatus, type WaitForSceneGraphAssertionOptions, assertMediaPlayerContainer, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, buildDebugCommand, captureDebugConsole, checkDevice, discoverRokuDevices, escapeXmlAttribute, getDeviceInfo, installPackage, isActiveMediaPlayerState, isCompleteSceneGraph, isNamedNodeVisible, launchApp, packageChannel, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerSafe, queryMediaPlayerXml, queryMediaPlayerXmlSafe, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, runDebugCommand, sceneGraphContainsText, takeScreenshot, validateEcpPath, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
|
|
171
|
+
export { type ActiveApp, type DebugCommandResult, type DebugConsoleCapture, type DeviceSummary, type DiscoveredRokuDevice, type MediaPlayerInfo, type MediaPlayerState, type NodeExpectation, type NodeState, type PackageResult, type RemoteKey, type RetryOptions, type RokuContext, type RokuDebugCommand, type RokuDebugPort, type SceneGraphAssertion, type SceneGraphBounds, type SceneGraphPoint, type SceneGraphStatus, type ScreenshotCaptureOptions, type WaitForSceneGraphAssertionOptions, assertMediaPlayerContainer, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, buildDebugCommand, captureDebugConsole, captureScreenshot, checkDevice, discoverRokuDevices, escapeXmlAttribute, getDeviceInfo, installPackage, isActiveMediaPlayerState, isCompleteSceneGraph, isNamedNodeVisible, launchApp, packageChannel, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerSafe, queryMediaPlayerXml, queryMediaPlayerXmlSafe, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, runDebugCommand, sceneGraphContainsText, takeScreenshot, validateEcpPath, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
export { assertMediaPlayerContainer, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, buildDebugCommand, captureDebugConsole, checkDevice, discoverRokuDevices, escapeXmlAttribute, getDeviceInfo, installPackage, isActiveMediaPlayerState, isCompleteSceneGraph, isNamedNodeVisible, launchApp, packageChannel, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerSafe, queryMediaPlayerXml, queryMediaPlayerXmlSafe, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, runDebugCommand, sceneGraphContainsText, takeScreenshot, validateEcpPath, validateRemoteKey, waitForActiveApp, waitForMediaPlayerState, waitForSceneGraphAssertion, waitForSceneGraphNode };
|
|
1
|
+
import { $ as readNamedNodeAttributes, A as readMediaPlayerContainer, B as waitForSceneGraphAssertion, C as queryActiveApp, D as queryMediaPlayerXml, E as queryMediaPlayerSafe, F as takeScreenshot, G as assertNamedNodeText, H as assertNamedNode, I as validateEcpPath, J as escapeXmlAttribute, K as assertNamedNodeTranslation, L as validateRemoteKey, M as readMediaPlayerPositionMs, N as readMediaPlayerState, O as queryMediaPlayerXmlSafe, Q as readNamedNodeAttribute, R as waitForActiveApp, S as pressKey, T as queryMediaPlayer, U as assertNamedNodeSize, V as waitForSceneGraphNode, W as assertNamedNodeState, X as isNamedNodeVisible, Y as isCompleteSceneGraph, Z as parseSceneGraphNumberList, _ as getDeviceInfo, at as sceneGraphContainsText, b as launchApp, ct as readXmlTag, et as readNamedNodeBounds, f as assertMediaPlayerContainer, g as discoverRokuDevices, h as checkDevice, it as readSceneGraphStatus, j as readMediaPlayerInfo, k as querySceneGraph, m as captureScreenshot, n as captureDebugConsole, nt as readNamedNodeTranslation, ot as readActiveApp, p as assertSceneGraphNode, q as assertSceneGraphNumberNear, r as runDebugCommand, rt as readSceneGraphFailure, st as readXmlAttribute, t as buildDebugCommand, tt as readNamedNodeNumber, v as installPackage, w as queryEcp, x as packageChannel, y as isActiveMediaPlayerState, z as waitForMediaPlayerState } from "./debug-BbJdwNkW.mjs";
|
|
2
|
+
export { assertMediaPlayerContainer, assertNamedNode, assertNamedNodeSize, assertNamedNodeState, assertNamedNodeText, assertNamedNodeTranslation, assertSceneGraphNode, assertSceneGraphNumberNear, buildDebugCommand, captureDebugConsole, captureScreenshot, checkDevice, discoverRokuDevices, escapeXmlAttribute, getDeviceInfo, installPackage, isActiveMediaPlayerState, isCompleteSceneGraph, isNamedNodeVisible, launchApp, packageChannel, parseSceneGraphNumberList, pressKey, queryActiveApp, queryEcp, queryMediaPlayer, queryMediaPlayerSafe, queryMediaPlayerXml, queryMediaPlayerXmlSafe, querySceneGraph, readActiveApp, readMediaPlayerContainer, readMediaPlayerInfo, readMediaPlayerPositionMs, readMediaPlayerState, readNamedNodeAttribute, readNamedNodeAttributes, readNamedNodeBounds, readNamedNodeNumber, readNamedNodeTranslation, readSceneGraphFailure, readSceneGraphStatus, readXmlAttribute, readXmlTag, runDebugCommand, sceneGraphContainsText, takeScreenshot, validateEcpPath, 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 queryActiveApp, L as validateRemoteKey, P as resolvePackageOutputPath, R as waitForActiveApp, S as pressKey, T as queryMediaPlayer, V as waitForSceneGraphNode, _ as getDeviceInfo, a as loadEnv, b as launchApp, c as requirePassword, d as resolveOutputPath, g as discoverRokuDevices, h as checkDevice, i as fail, it as readSceneGraphStatus, k as querySceneGraph, l as requireTarget, lt as normalizeError, m as captureScreenshot, n as captureDebugConsole, o as loadLocalEnv, p as assertSceneGraphNode, r as runDebugCommand, rt as readSceneGraphFailure, s as rejectUnsafeEcpPath, t as buildDebugCommand, u as resolveFileOutputPath, ut as renderError, v as installPackage, w as queryEcp, x as packageChannel, z as waitForMediaPlayerState } from "./debug-BbJdwNkW.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -255,8 +255,7 @@ const runCommand = async (context, command, dryRun) => {
|
|
|
255
255
|
const path = timestampOutputPath(resolveFileOutputPath(command.outputPath, "screenshot output path"));
|
|
256
256
|
if (dryRun) return dryRunResult(command.name, { path });
|
|
257
257
|
const password = requirePassword(deviceContext);
|
|
258
|
-
|
|
259
|
-
const screenshotPath = await takeScreenshot({
|
|
258
|
+
const screenshotPath = await captureScreenshot({
|
|
260
259
|
...deviceContext,
|
|
261
260
|
password
|
|
262
261
|
}, path);
|
|
@@ -405,7 +404,7 @@ const writeProof = async (context, outputDir, includeScreenshot) => {
|
|
|
405
404
|
writeJson("media-player", await observe(async () => await queryMediaPlayer(context)));
|
|
406
405
|
if (includeScreenshot) {
|
|
407
406
|
const password = requirePassword(context);
|
|
408
|
-
const path = await
|
|
407
|
+
const path = await captureScreenshot({
|
|
409
408
|
...context,
|
|
410
409
|
password
|
|
411
410
|
}, timestampOutputPath(`${outputDir}/screenshot.png`));
|