@qawolf/cli 1.26.0 → 1.27.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/dist/cli.js
CHANGED
|
@@ -8787,7 +8787,17 @@ var interactMessages = {
|
|
|
8787
8787
|
actionMayHaveHappened: "The runner could not be reached, which does not mean the action was not performed: it may have stopped answering mid-action. Take a screenshot before repeating it.",
|
|
8788
8788
|
actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
|
|
8789
8789
|
actionNotSupportedOnMobile: (type) => `A mobile runner has a touchscreen, so it cannot perform ${type} as asked. It taps with a left-button click, swipes with drag, and types into whatever the last tap focused.`,
|
|
8790
|
+
actionFailedScreenshotToStdout: "Its screen was written to stdout as a JPEG, so look at that rather than sending the action again.",
|
|
8791
|
+
actionFailedScreenshotUnwritten: (detail) => `Its screen could not be written, because ${detail}, so take one with qawolf runner screenshot rather than sending the action again.`,
|
|
8792
|
+
actionFailedScreenshotWritten: (path) => `Its screen was written to ${path}, so look at that rather than sending the action again.`,
|
|
8793
|
+
actionFailedWithoutScreenshot: "The runner answered without a screen, so nothing was written; take one with qawolf runner screenshot rather than sending the action again.",
|
|
8790
8794
|
actionPerformed: (type) => `Performed ${type}.`,
|
|
8795
|
+
actionPerformedScreenshotNotAnImage: (type) => `Performed ${type}, but the screen that came with the answer was not a JPEG, so nothing was written. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead, and report it if it keeps happening.`,
|
|
8796
|
+
actionPerformedScreenshotToStdout: (type) => `Performed ${type} and wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.`,
|
|
8797
|
+
actionPerformedScreenshotStdoutUnwritable: (type, detail) => `Performed ${type}, but its screen could not be written to stdout: ${detail}. The action took effect, so do not repeat it: keep the pipe reading stdout open, or take the screen with qawolf runner screenshot --out <file>.`,
|
|
8798
|
+
actionPerformedScreenshotUnwritable: (type, path, detail) => `Performed ${type}, but its screen could not be written to "${path}": ${detail}. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot, giving --out a path this process can write to.`,
|
|
8799
|
+
actionPerformedScreenshotWritten: (type, path) => `Performed ${type} and wrote the runner's screen to ${path}.`,
|
|
8800
|
+
actionPerformedWithoutScreenshot: (type) => `Performed ${type}, but the runner answered without the screen it was asked for. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead. If this keeps happening, the platform or this CLI is behind the other: upgrade with npm install -g @qawolf/cli.`,
|
|
8791
8801
|
actionAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
8792
8802
|
screenshotAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
8793
8803
|
runnerHasNoScreen: "This runner does not run a browser on a virtual desktop, so there is nothing about it to see or drive. Retrying will never help: launch a playwright runner instead.",
|
|
@@ -8799,8 +8809,11 @@ var interactMessages = {
|
|
|
8799
8809
|
screenNeedsARun: "This runner has not run anything yet, so its screen has never started. Waiting will not clear this and there is nothing to retry: run a flow on it with qawolf runner run, then ask again. Evaluating a snippet does not start a screen.",
|
|
8800
8810
|
screenNotReady: "The runner has a screen and cannot serve this yet. Its virtual desktop restarts when a run changes the display size, and it serves one request at a time, so something already in flight is the usual reason. Retry in a second or two.",
|
|
8801
8811
|
screenshotNotAnImage: "The screen was captured but did not arrive as a JPEG, so nothing was written. Nothing about the command needs changing: try it again, and report it if it keeps happening.",
|
|
8812
|
+
stdoutIsATerminal: (flag) => `Stdout is a terminal, so the JPEG bytes would have nowhere to go. Redirect stdout to a file or pipe it into a reader, or give ${flag} a file path instead of "-".`,
|
|
8813
|
+
screenshotStdoutUnwritable: (detail) => `The screen was captured but could not be written to stdout: ${detail}. Keep the pipe reading stdout open, or give --out a file path instead of "-".`,
|
|
8802
8814
|
screenshotUnwritable: (path, detail) => `The screen was captured but could not be written to "${path}": ${detail}. Give --out a path this process can write to.`,
|
|
8803
8815
|
screenshotWritten: (path) => `Wrote the runner's screen to ${path}.`,
|
|
8816
|
+
screenshotWrittenToStdout: "Wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.",
|
|
8804
8817
|
snippetEmpty: (path) => `"${path}" holds no code to evaluate.`,
|
|
8805
8818
|
snippetErrored: (errorMessage) => errorMessage === undefined ? "The snippet threw and reported no message." : `The snippet threw: ${errorMessage}`,
|
|
8806
8819
|
snippetFileUnreadable: (path) => `Could not read "${path}". Name a readable file, or "-" to read the snippet from stdin.`,
|
|
@@ -20551,7 +20564,7 @@ function startUpdateCheck(deps) {
|
|
|
20551
20564
|
// package.json
|
|
20552
20565
|
var package_default = {
|
|
20553
20566
|
name: "@qawolf/cli",
|
|
20554
|
-
version: "1.
|
|
20567
|
+
version: "1.27.0",
|
|
20555
20568
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
20556
20569
|
keywords: [
|
|
20557
20570
|
"automation",
|
|
@@ -32308,6 +32321,28 @@ function registerPublicApiCommands(program, signals, options = {}) {
|
|
|
32308
32321
|
}
|
|
32309
32322
|
}
|
|
32310
32323
|
|
|
32324
|
+
// src/domains/interactiveRunner/evaluateSnippetFailure.ts
|
|
32325
|
+
function describeEvaluateSnippetFailure(failure) {
|
|
32326
|
+
const { failureReason } = failure;
|
|
32327
|
+
switch (failureReason) {
|
|
32328
|
+
case "runner-cannot-evaluate-snippets":
|
|
32329
|
+
return {
|
|
32330
|
+
error: interactiveRunnerMessages.runnerCannotEvaluateSnippets,
|
|
32331
|
+
exitCode: exitCodes.invalidArgs
|
|
32332
|
+
};
|
|
32333
|
+
case "runner-unreachable":
|
|
32334
|
+
return {
|
|
32335
|
+
error: interactiveRunnerMessages.snippetRunnerUnreachable,
|
|
32336
|
+
exitCode: exitCodes.network
|
|
32337
|
+
};
|
|
32338
|
+
default:
|
|
32339
|
+
return {
|
|
32340
|
+
error: interactiveRunnerMessages.snippetAnsweredUnknown(failureReason),
|
|
32341
|
+
exitCode: exitCodes.network
|
|
32342
|
+
};
|
|
32343
|
+
}
|
|
32344
|
+
}
|
|
32345
|
+
|
|
32311
32346
|
// src/domains/interactiveRunner/runnerCallOptions.ts
|
|
32312
32347
|
var runnerCallOptions = { timeoutMs: 90000 };
|
|
32313
32348
|
|
|
@@ -32420,67 +32455,138 @@ function announceRunner(ctx, resolved) {
|
|
|
32420
32455
|
}
|
|
32421
32456
|
}
|
|
32422
32457
|
|
|
32423
|
-
// src/
|
|
32424
|
-
|
|
32425
|
-
|
|
32426
|
-
|
|
32427
|
-
|
|
32428
|
-
|
|
32429
|
-
|
|
32458
|
+
// src/core/interactiveRunner/runFiles.ts
|
|
32459
|
+
import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve11, sep as sep5 } from "node:path";
|
|
32460
|
+
function toCollectedPath(cwd, path) {
|
|
32461
|
+
const absolute = isAbsolute5(path) ? path : resolve11(cwd, path);
|
|
32462
|
+
return relative10(cwd, absolute).split(sep5).join("/");
|
|
32463
|
+
}
|
|
32464
|
+
var namedLargestFileCount = 3;
|
|
32465
|
+
function largestFiles(files) {
|
|
32466
|
+
return Object.entries(files).map(([path, content]) => ({
|
|
32467
|
+
byteLength: runFilesByteLength({ [path]: content }),
|
|
32468
|
+
path
|
|
32469
|
+
})).sort((a, b) => b.byteLength - a.byteLength).slice(0, namedLargestFileCount);
|
|
32470
|
+
}
|
|
32471
|
+
function checkPresenceAndSize(files, requiredPath) {
|
|
32472
|
+
if (!Object.hasOwn(files, requiredPath)) {
|
|
32473
|
+
return { path: requiredPath, type: "missing-file" };
|
|
32474
|
+
}
|
|
32475
|
+
const byteLength = runFilesByteLength(files);
|
|
32476
|
+
if (byteLength > maxRunFilesByteLength) {
|
|
32477
|
+
return {
|
|
32478
|
+
byteLength,
|
|
32479
|
+
largest: largestFiles(files),
|
|
32480
|
+
maxByteLength: maxRunFilesByteLength,
|
|
32481
|
+
type: "too-large"
|
|
32482
|
+
};
|
|
32483
|
+
}
|
|
32484
|
+
const encodedByteLength = Buffer.byteLength(JSON.stringify({ files, path: requiredPath }), "utf8");
|
|
32485
|
+
if (encodedByteLength > maxRunnerRequestEncodedByteLength) {
|
|
32486
|
+
return {
|
|
32487
|
+
byteLength: encodedByteLength,
|
|
32488
|
+
maxByteLength: maxRunnerRequestEncodedByteLength,
|
|
32489
|
+
type: "request-too-large"
|
|
32490
|
+
};
|
|
32491
|
+
}
|
|
32492
|
+
return { type: "ok" };
|
|
32493
|
+
}
|
|
32494
|
+
function checkRunFiles(files, entryPointPath) {
|
|
32495
|
+
const present = checkPresenceAndSize(files, entryPointPath);
|
|
32496
|
+
if (present.type !== "ok")
|
|
32497
|
+
return present;
|
|
32498
|
+
if (!Object.hasOwn(files, runPackageJsonPath)) {
|
|
32499
|
+
return { type: "missing-package-json" };
|
|
32500
|
+
}
|
|
32501
|
+
return { type: "ok" };
|
|
32502
|
+
}
|
|
32503
|
+
function checkSnippetFiles(files, contextFilePath) {
|
|
32504
|
+
return checkPresenceAndSize(files, contextFilePath);
|
|
32505
|
+
}
|
|
32506
|
+
function describeRunFilesCheck(check) {
|
|
32507
|
+
switch (check.type) {
|
|
32508
|
+
case "missing-file":
|
|
32509
|
+
return interactiveRunnerMessages.fileNotCollected(check.path);
|
|
32510
|
+
case "missing-package-json":
|
|
32511
|
+
return interactiveRunnerMessages.missingPackageJson;
|
|
32512
|
+
case "too-large":
|
|
32513
|
+
return interactiveRunnerMessages.filesTooLarge(check.byteLength, check.maxByteLength, check.largest);
|
|
32514
|
+
case "request-too-large":
|
|
32515
|
+
return interactiveRunnerMessages.requestTooLarge(check.byteLength, check.maxByteLength);
|
|
32516
|
+
}
|
|
32517
|
+
}
|
|
32518
|
+
|
|
32519
|
+
// src/domains/interactiveRunner/snippetScope.ts
|
|
32520
|
+
async function resolveSnippetScope(contextFile, deps) {
|
|
32521
|
+
if (contextFile === undefined) {
|
|
32522
|
+
return { filePath: undefined, files: undefined, ok: true };
|
|
32523
|
+
}
|
|
32524
|
+
const filePath = toCollectedPath(deps.cwd, contextFile);
|
|
32525
|
+
const { files } = await deps.collectRunFiles([filePath]);
|
|
32526
|
+
const check = checkSnippetFiles(files, filePath);
|
|
32527
|
+
if (check.type !== "ok") {
|
|
32528
|
+
return { error: describeRunFilesCheck(check), ok: false };
|
|
32529
|
+
}
|
|
32530
|
+
return { filePath, files, ok: true };
|
|
32531
|
+
}
|
|
32532
|
+
|
|
32533
|
+
// src/domains/interactiveRunner/evaluateSnippet.ts
|
|
32534
|
+
var stdinArgument = "-";
|
|
32535
|
+
async function readCode(source, deps) {
|
|
32536
|
+
const code = source === stdinArgument ? await deps.readStdin() : await deps.readFile(source).catch(() => {
|
|
32537
|
+
return;
|
|
32538
|
+
});
|
|
32539
|
+
if (code === undefined) {
|
|
32540
|
+
return {
|
|
32541
|
+
error: interactiveRunnerMessages.snippetFileUnreadable(source),
|
|
32542
|
+
ok: false
|
|
32543
|
+
};
|
|
32544
|
+
}
|
|
32545
|
+
if (code.trim() === "") {
|
|
32546
|
+
return source === stdinArgument ? { error: interactiveRunnerMessages.stdinEmptySnippet, ok: false } : { error: interactiveRunnerMessages.snippetEmpty(source), ok: false };
|
|
32547
|
+
}
|
|
32548
|
+
return { code, ok: true };
|
|
32549
|
+
}
|
|
32550
|
+
async function handleRunnerExec(ctx, options, deps) {
|
|
32551
|
+
const code = await readCode(options.source, deps);
|
|
32552
|
+
if (!code.ok)
|
|
32553
|
+
return { error: code.error, exitCode: exitCodes.invalidArgs };
|
|
32554
|
+
const scope = await resolveSnippetScope(options.contextFile, deps);
|
|
32555
|
+
if (!scope.ok)
|
|
32556
|
+
return { error: scope.error, exitCode: exitCodes.invalidArgs };
|
|
32557
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
32430
32558
|
if (resolved.type === "failed") {
|
|
32431
32559
|
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
32432
32560
|
}
|
|
32433
|
-
|
|
32434
|
-
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.
|
|
32561
|
+
announceRunner(ctx, resolved);
|
|
32562
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
|
|
32563
|
+
code: code.code,
|
|
32564
|
+
id: resolved.runnerId,
|
|
32565
|
+
...scope.filePath === undefined ? {} : { filePath: scope.filePath },
|
|
32566
|
+
...scope.files === undefined ? {} : { files: scope.files }
|
|
32567
|
+
}, runnerCallOptions);
|
|
32435
32568
|
if (!result.ok) {
|
|
32436
|
-
return {
|
|
32569
|
+
return {
|
|
32570
|
+
...failureFields(result),
|
|
32571
|
+
exitCode: result.exitCode ?? exitCodes.network
|
|
32572
|
+
};
|
|
32437
32573
|
}
|
|
32438
|
-
|
|
32439
|
-
|
|
32440
|
-
case "cleared":
|
|
32441
|
-
ctx.ui.output(result.value, interactiveRunnerMessages.highlightCleared);
|
|
32442
|
-
return;
|
|
32443
|
-
case "success": {
|
|
32444
|
-
const answer = result.value;
|
|
32445
|
-
if (answer.status === "invalid") {
|
|
32446
|
-
return {
|
|
32447
|
-
error: interactiveRunnerMessages.highlightSelectorInvalid(answer.selector),
|
|
32448
|
-
exitCode: exitCodes.invalidArgs
|
|
32449
|
-
};
|
|
32450
|
-
}
|
|
32451
|
-
ctx.ui.output(answer, answer.status === "empty" ? interactiveRunnerMessages.highlightMatchedNothing(answer.selector) : interactiveRunnerMessages.highlightMatched(answer.matchCount, answer.targetPage));
|
|
32452
|
-
return;
|
|
32453
|
-
}
|
|
32454
|
-
case "failure":
|
|
32455
|
-
return describeFailure(result.value.failureReason);
|
|
32456
|
-
default:
|
|
32457
|
-
return {
|
|
32458
|
-
error: interactiveRunnerMessages.highlightAnsweredUnknown(outcome),
|
|
32459
|
-
exitCode: exitCodes.network
|
|
32460
|
-
};
|
|
32574
|
+
if (result.value.outcome === "failure") {
|
|
32575
|
+
return describeEvaluateSnippetFailure(result.value);
|
|
32461
32576
|
}
|
|
32462
|
-
|
|
32463
|
-
|
|
32464
|
-
|
|
32465
|
-
|
|
32466
|
-
|
|
32467
|
-
error: interactiveRunnerMessages.highlightNoAnswer,
|
|
32468
|
-
exitCode: exitCodes.network
|
|
32469
|
-
};
|
|
32470
|
-
case "runner-cannot-highlight-selectors":
|
|
32471
|
-
return {
|
|
32472
|
-
error: interactiveRunnerMessages.runnerCannotHighlightSelectors,
|
|
32473
|
-
exitCode: exitCodes.invalidArgs
|
|
32474
|
-
};
|
|
32475
|
-
case "runner-unreachable":
|
|
32577
|
+
switch (result.value.result) {
|
|
32578
|
+
case "success":
|
|
32579
|
+
ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
|
|
32580
|
+
return;
|
|
32581
|
+
case "stopped":
|
|
32476
32582
|
return {
|
|
32477
|
-
error: interactiveRunnerMessages.
|
|
32478
|
-
exitCode: exitCodes.
|
|
32583
|
+
error: interactiveRunnerMessages.snippetStopped,
|
|
32584
|
+
exitCode: exitCodes.testFailure
|
|
32479
32585
|
};
|
|
32480
|
-
|
|
32586
|
+
case "error":
|
|
32481
32587
|
return {
|
|
32482
|
-
error: interactiveRunnerMessages.
|
|
32483
|
-
exitCode: exitCodes.
|
|
32588
|
+
error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
|
|
32589
|
+
exitCode: exitCodes.testFailure
|
|
32484
32590
|
};
|
|
32485
32591
|
}
|
|
32486
32592
|
}
|
|
@@ -32823,19 +32929,38 @@ function makeRunnerStore(options) {
|
|
|
32823
32929
|
// src/shell/interactiveRunner/writeScreenshot.ts
|
|
32824
32930
|
import { dirname as dirname16 } from "node:path";
|
|
32825
32931
|
var jpegStartOfImage = [255, 216, 255];
|
|
32932
|
+
var stdoutPath = "-";
|
|
32826
32933
|
async function writeScreenshot(options) {
|
|
32827
32934
|
const bytes = Buffer.from(options.imageJpegBase64, "base64");
|
|
32828
32935
|
if (jpegStartOfImage.some((byte, index) => bytes[index] !== byte)) {
|
|
32829
32936
|
return { ok: false, reason: "not-a-jpeg" };
|
|
32830
32937
|
}
|
|
32831
32938
|
try {
|
|
32832
|
-
|
|
32833
|
-
|
|
32939
|
+
if (options.path === stdoutPath) {
|
|
32940
|
+
await writeToStdout(options.stdout ?? process.stdout, bytes);
|
|
32941
|
+
} else {
|
|
32942
|
+
await options.fs.mkdir(dirname16(options.path), { recursive: true });
|
|
32943
|
+
await options.fs.writeFile(options.path, bytes);
|
|
32944
|
+
}
|
|
32834
32945
|
} catch (error) {
|
|
32835
32946
|
return { detail: errorMessage(error), ok: false, reason: "unwritable" };
|
|
32836
32947
|
}
|
|
32837
32948
|
return { ok: true };
|
|
32838
32949
|
}
|
|
32950
|
+
function writeToStdout(stdout, bytes) {
|
|
32951
|
+
return new Promise((resolve, reject) => {
|
|
32952
|
+
const onError = (error) => reject(error);
|
|
32953
|
+
stdout.once("error", onError);
|
|
32954
|
+
stdout.write(bytes, (error) => {
|
|
32955
|
+
if (error) {
|
|
32956
|
+
reject(error);
|
|
32957
|
+
return;
|
|
32958
|
+
}
|
|
32959
|
+
stdout.off("error", onError);
|
|
32960
|
+
resolve();
|
|
32961
|
+
});
|
|
32962
|
+
});
|
|
32963
|
+
}
|
|
32839
32964
|
|
|
32840
32965
|
// src/shell/stdin.ts
|
|
32841
32966
|
async function readStdin2() {
|
|
@@ -32876,6 +33001,81 @@ function runnerDeps(ctx) {
|
|
|
32876
33001
|
});
|
|
32877
33002
|
}
|
|
32878
33003
|
|
|
33004
|
+
// src/commands/runner/exec.register.ts
|
|
33005
|
+
var execExamples = `
|
|
33006
|
+
Examples:
|
|
33007
|
+
$ qawolf runner exec snippet.ts
|
|
33008
|
+
$ echo 'console.log(await page.title())' | qawolf runner exec -
|
|
33009
|
+
$ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
|
|
33010
|
+
function registerRunnerExecCommand(runner, signals) {
|
|
33011
|
+
declareCommandKind(runner.command("exec <file>"), "write").description("Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin").option("--file <path>", "File whose scope the snippet is evaluated in; it and the directory's other files travel with it").option("--runner <id>", runnerFlagDescription).addHelpText("after", execExamples).action((file, opts, command) => withAuthContext(signals, (ctx) => handleRunnerExec(ctx, { contextFile: opts.file, runner: opts.runner, source: file }, runnerDeps(ctx)))(opts, command));
|
|
33012
|
+
}
|
|
33013
|
+
|
|
33014
|
+
// src/domains/interactiveRunner/highlightSelector.ts
|
|
33015
|
+
async function handleRunnerHighlightSelector(ctx, options, deps) {
|
|
33016
|
+
const resolved = await resolveRunner(ctx, {
|
|
33017
|
+
autoLaunch: false,
|
|
33018
|
+
noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForHighlight,
|
|
33019
|
+
runner: options.runner
|
|
33020
|
+
}, deps);
|
|
33021
|
+
if (resolved.type === "failed") {
|
|
33022
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
33023
|
+
}
|
|
33024
|
+
const selector = options.selector ?? "";
|
|
33025
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.highlightSelector, { id: resolved.runnerId, selector }, runnerCallOptions);
|
|
33026
|
+
if (!result.ok) {
|
|
33027
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
33028
|
+
}
|
|
33029
|
+
const { outcome } = result.value;
|
|
33030
|
+
switch (outcome) {
|
|
33031
|
+
case "cleared":
|
|
33032
|
+
ctx.ui.output(result.value, interactiveRunnerMessages.highlightCleared);
|
|
33033
|
+
return;
|
|
33034
|
+
case "success": {
|
|
33035
|
+
const answer = result.value;
|
|
33036
|
+
if (answer.status === "invalid") {
|
|
33037
|
+
return {
|
|
33038
|
+
error: interactiveRunnerMessages.highlightSelectorInvalid(answer.selector),
|
|
33039
|
+
exitCode: exitCodes.invalidArgs
|
|
33040
|
+
};
|
|
33041
|
+
}
|
|
33042
|
+
ctx.ui.output(answer, answer.status === "empty" ? interactiveRunnerMessages.highlightMatchedNothing(answer.selector) : interactiveRunnerMessages.highlightMatched(answer.matchCount, answer.targetPage));
|
|
33043
|
+
return;
|
|
33044
|
+
}
|
|
33045
|
+
case "failure":
|
|
33046
|
+
return describeFailure(result.value.failureReason);
|
|
33047
|
+
default:
|
|
33048
|
+
return {
|
|
33049
|
+
error: interactiveRunnerMessages.highlightAnsweredUnknown(outcome),
|
|
33050
|
+
exitCode: exitCodes.network
|
|
33051
|
+
};
|
|
33052
|
+
}
|
|
33053
|
+
}
|
|
33054
|
+
function describeFailure(failureReason) {
|
|
33055
|
+
switch (failureReason) {
|
|
33056
|
+
case "no-answer":
|
|
33057
|
+
return {
|
|
33058
|
+
error: interactiveRunnerMessages.highlightNoAnswer,
|
|
33059
|
+
exitCode: exitCodes.network
|
|
33060
|
+
};
|
|
33061
|
+
case "runner-cannot-highlight-selectors":
|
|
33062
|
+
return {
|
|
33063
|
+
error: interactiveRunnerMessages.runnerCannotHighlightSelectors,
|
|
33064
|
+
exitCode: exitCodes.invalidArgs
|
|
33065
|
+
};
|
|
33066
|
+
case "runner-unreachable":
|
|
33067
|
+
return {
|
|
33068
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
33069
|
+
exitCode: exitCodes.network
|
|
33070
|
+
};
|
|
33071
|
+
default:
|
|
33072
|
+
return {
|
|
33073
|
+
error: interactiveRunnerMessages.highlightAnsweredUnknown(failureReason),
|
|
33074
|
+
exitCode: exitCodes.network
|
|
33075
|
+
};
|
|
33076
|
+
}
|
|
33077
|
+
}
|
|
33078
|
+
|
|
32879
33079
|
// src/commands/runner/highlightSelector.register.ts
|
|
32880
33080
|
var highlightExamples = `
|
|
32881
33081
|
Examples:
|
|
@@ -33265,162 +33465,10 @@ function registerRunnerInspectCommands(runner, signals) {
|
|
|
33265
33465
|
registerRunnerInspectMobileCommands(inspect, signals);
|
|
33266
33466
|
}
|
|
33267
33467
|
|
|
33268
|
-
// src/
|
|
33269
|
-
function
|
|
33270
|
-
const
|
|
33271
|
-
|
|
33272
|
-
case "runner-cannot-evaluate-snippets":
|
|
33273
|
-
return {
|
|
33274
|
-
error: interactiveRunnerMessages.runnerCannotEvaluateSnippets,
|
|
33275
|
-
exitCode: exitCodes.invalidArgs
|
|
33276
|
-
};
|
|
33277
|
-
case "runner-unreachable":
|
|
33278
|
-
return {
|
|
33279
|
-
error: interactiveRunnerMessages.snippetRunnerUnreachable,
|
|
33280
|
-
exitCode: exitCodes.network
|
|
33281
|
-
};
|
|
33282
|
-
default:
|
|
33283
|
-
return {
|
|
33284
|
-
error: interactiveRunnerMessages.snippetAnsweredUnknown(failureReason),
|
|
33285
|
-
exitCode: exitCodes.network
|
|
33286
|
-
};
|
|
33287
|
-
}
|
|
33288
|
-
}
|
|
33289
|
-
|
|
33290
|
-
// src/core/interactiveRunner/runFiles.ts
|
|
33291
|
-
import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve11, sep as sep5 } from "node:path";
|
|
33292
|
-
function toCollectedPath(cwd, path) {
|
|
33293
|
-
const absolute = isAbsolute5(path) ? path : resolve11(cwd, path);
|
|
33294
|
-
return relative10(cwd, absolute).split(sep5).join("/");
|
|
33295
|
-
}
|
|
33296
|
-
var namedLargestFileCount = 3;
|
|
33297
|
-
function largestFiles(files) {
|
|
33298
|
-
return Object.entries(files).map(([path, content]) => ({
|
|
33299
|
-
byteLength: runFilesByteLength({ [path]: content }),
|
|
33300
|
-
path
|
|
33301
|
-
})).sort((a, b) => b.byteLength - a.byteLength).slice(0, namedLargestFileCount);
|
|
33302
|
-
}
|
|
33303
|
-
function checkPresenceAndSize(files, requiredPath) {
|
|
33304
|
-
if (!Object.hasOwn(files, requiredPath)) {
|
|
33305
|
-
return { path: requiredPath, type: "missing-file" };
|
|
33306
|
-
}
|
|
33307
|
-
const byteLength = runFilesByteLength(files);
|
|
33308
|
-
if (byteLength > maxRunFilesByteLength) {
|
|
33309
|
-
return {
|
|
33310
|
-
byteLength,
|
|
33311
|
-
largest: largestFiles(files),
|
|
33312
|
-
maxByteLength: maxRunFilesByteLength,
|
|
33313
|
-
type: "too-large"
|
|
33314
|
-
};
|
|
33315
|
-
}
|
|
33316
|
-
const encodedByteLength = Buffer.byteLength(JSON.stringify({ files, path: requiredPath }), "utf8");
|
|
33317
|
-
if (encodedByteLength > maxRunnerRequestEncodedByteLength) {
|
|
33318
|
-
return {
|
|
33319
|
-
byteLength: encodedByteLength,
|
|
33320
|
-
maxByteLength: maxRunnerRequestEncodedByteLength,
|
|
33321
|
-
type: "request-too-large"
|
|
33322
|
-
};
|
|
33323
|
-
}
|
|
33324
|
-
return { type: "ok" };
|
|
33325
|
-
}
|
|
33326
|
-
function checkRunFiles(files, entryPointPath) {
|
|
33327
|
-
const present = checkPresenceAndSize(files, entryPointPath);
|
|
33328
|
-
if (present.type !== "ok")
|
|
33329
|
-
return present;
|
|
33330
|
-
if (!Object.hasOwn(files, runPackageJsonPath)) {
|
|
33331
|
-
return { type: "missing-package-json" };
|
|
33332
|
-
}
|
|
33333
|
-
return { type: "ok" };
|
|
33334
|
-
}
|
|
33335
|
-
function checkSnippetFiles(files, contextFilePath) {
|
|
33336
|
-
return checkPresenceAndSize(files, contextFilePath);
|
|
33337
|
-
}
|
|
33338
|
-
function describeRunFilesCheck(check) {
|
|
33339
|
-
switch (check.type) {
|
|
33340
|
-
case "missing-file":
|
|
33341
|
-
return interactiveRunnerMessages.fileNotCollected(check.path);
|
|
33342
|
-
case "missing-package-json":
|
|
33343
|
-
return interactiveRunnerMessages.missingPackageJson;
|
|
33344
|
-
case "too-large":
|
|
33345
|
-
return interactiveRunnerMessages.filesTooLarge(check.byteLength, check.maxByteLength, check.largest);
|
|
33346
|
-
case "request-too-large":
|
|
33347
|
-
return interactiveRunnerMessages.requestTooLarge(check.byteLength, check.maxByteLength);
|
|
33348
|
-
}
|
|
33349
|
-
}
|
|
33350
|
-
|
|
33351
|
-
// src/domains/interactiveRunner/snippetScope.ts
|
|
33352
|
-
async function resolveSnippetScope(contextFile, deps) {
|
|
33353
|
-
if (contextFile === undefined) {
|
|
33354
|
-
return { filePath: undefined, files: undefined, ok: true };
|
|
33355
|
-
}
|
|
33356
|
-
const filePath = toCollectedPath(deps.cwd, contextFile);
|
|
33357
|
-
const { files } = await deps.collectRunFiles([filePath]);
|
|
33358
|
-
const check = checkSnippetFiles(files, filePath);
|
|
33359
|
-
if (check.type !== "ok") {
|
|
33360
|
-
return { error: describeRunFilesCheck(check), ok: false };
|
|
33361
|
-
}
|
|
33362
|
-
return { filePath, files, ok: true };
|
|
33363
|
-
}
|
|
33364
|
-
|
|
33365
|
-
// src/domains/interactiveRunner/evaluateSnippet.ts
|
|
33366
|
-
var stdinArgument = "-";
|
|
33367
|
-
async function readCode(source, deps) {
|
|
33368
|
-
const code = source === stdinArgument ? await deps.readStdin() : await deps.readFile(source).catch(() => {
|
|
33369
|
-
return;
|
|
33370
|
-
});
|
|
33371
|
-
if (code === undefined) {
|
|
33372
|
-
return {
|
|
33373
|
-
error: interactiveRunnerMessages.snippetFileUnreadable(source),
|
|
33374
|
-
ok: false
|
|
33375
|
-
};
|
|
33376
|
-
}
|
|
33377
|
-
if (code.trim() === "") {
|
|
33378
|
-
return source === stdinArgument ? { error: interactiveRunnerMessages.stdinEmptySnippet, ok: false } : { error: interactiveRunnerMessages.snippetEmpty(source), ok: false };
|
|
33379
|
-
}
|
|
33380
|
-
return { code, ok: true };
|
|
33381
|
-
}
|
|
33382
|
-
async function handleRunnerExec(ctx, options, deps) {
|
|
33383
|
-
const code = await readCode(options.source, deps);
|
|
33384
|
-
if (!code.ok)
|
|
33385
|
-
return { error: code.error, exitCode: exitCodes.invalidArgs };
|
|
33386
|
-
const scope = await resolveSnippetScope(options.contextFile, deps);
|
|
33387
|
-
if (!scope.ok)
|
|
33388
|
-
return { error: scope.error, exitCode: exitCodes.invalidArgs };
|
|
33389
|
-
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
33390
|
-
if (resolved.type === "failed") {
|
|
33391
|
-
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
33392
|
-
}
|
|
33393
|
-
announceRunner(ctx, resolved);
|
|
33394
|
-
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
|
|
33395
|
-
code: code.code,
|
|
33396
|
-
id: resolved.runnerId,
|
|
33397
|
-
...scope.filePath === undefined ? {} : { filePath: scope.filePath },
|
|
33398
|
-
...scope.files === undefined ? {} : { files: scope.files }
|
|
33399
|
-
}, runnerCallOptions);
|
|
33400
|
-
if (!result.ok) {
|
|
33401
|
-
return {
|
|
33402
|
-
...failureFields(result),
|
|
33403
|
-
exitCode: result.exitCode ?? exitCodes.network
|
|
33404
|
-
};
|
|
33405
|
-
}
|
|
33406
|
-
if (result.value.outcome === "failure") {
|
|
33407
|
-
return describeEvaluateSnippetFailure(result.value);
|
|
33408
|
-
}
|
|
33409
|
-
switch (result.value.result) {
|
|
33410
|
-
case "success":
|
|
33411
|
-
ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
|
|
33412
|
-
return;
|
|
33413
|
-
case "stopped":
|
|
33414
|
-
return {
|
|
33415
|
-
error: interactiveRunnerMessages.snippetStopped,
|
|
33416
|
-
exitCode: exitCodes.testFailure
|
|
33417
|
-
};
|
|
33418
|
-
case "error":
|
|
33419
|
-
return {
|
|
33420
|
-
error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
|
|
33421
|
-
exitCode: exitCodes.testFailure
|
|
33422
|
-
};
|
|
33423
|
-
}
|
|
33468
|
+
// src/core/sentences.ts
|
|
33469
|
+
function appendSentence(text, sentence) {
|
|
33470
|
+
const punctuated = /[.!?]$/.test(text.trimEnd()) ? text.trimEnd() : `${text.trimEnd()}.`;
|
|
33471
|
+
return `${punctuated} ${sentence}`;
|
|
33424
33472
|
}
|
|
33425
33473
|
|
|
33426
33474
|
// src/domains/interactiveRunner/performActionFailure.ts
|
|
@@ -33467,6 +33515,56 @@ function describePerformActionFailure(options) {
|
|
|
33467
33515
|
}
|
|
33468
33516
|
}
|
|
33469
33517
|
|
|
33518
|
+
// src/domains/interactiveRunner/performActionScreenshot.ts
|
|
33519
|
+
async function writeActionScreenshot(ctx, options, deps) {
|
|
33520
|
+
const { type } = options.action;
|
|
33521
|
+
if (options.imageJpegBase64 === undefined) {
|
|
33522
|
+
return {
|
|
33523
|
+
error: interactiveRunnerMessages.actionPerformedWithoutScreenshot(type),
|
|
33524
|
+
exitCode: exitCodes.network
|
|
33525
|
+
};
|
|
33526
|
+
}
|
|
33527
|
+
const written = await deps.writeScreenshot({
|
|
33528
|
+
imageJpegBase64: options.imageJpegBase64,
|
|
33529
|
+
path: options.out
|
|
33530
|
+
});
|
|
33531
|
+
if (!written.ok) {
|
|
33532
|
+
return written.reason === "not-a-jpeg" ? {
|
|
33533
|
+
error: interactiveRunnerMessages.actionPerformedScreenshotNotAnImage(type),
|
|
33534
|
+
exitCode: exitCodes.network
|
|
33535
|
+
} : {
|
|
33536
|
+
error: options.out === stdoutPath ? interactiveRunnerMessages.actionPerformedScreenshotStdoutUnwritable(type, written.detail) : interactiveRunnerMessages.actionPerformedScreenshotUnwritable(type, options.out, written.detail),
|
|
33537
|
+
exitCode: exitCodes.network
|
|
33538
|
+
};
|
|
33539
|
+
}
|
|
33540
|
+
if (options.out === stdoutPath) {
|
|
33541
|
+
ctx.ui.success(interactiveRunnerMessages.actionPerformedScreenshotToStdout(type));
|
|
33542
|
+
return;
|
|
33543
|
+
}
|
|
33544
|
+
ctx.ui.output({ action: options.action, outcome: "success", screenshotPath: options.out }, interactiveRunnerMessages.actionPerformedScreenshotWritten(type, options.out));
|
|
33545
|
+
return;
|
|
33546
|
+
}
|
|
33547
|
+
async function addFailureScreenshot(options, deps) {
|
|
33548
|
+
const note = await describeFailureScreenshot(options, deps);
|
|
33549
|
+
return {
|
|
33550
|
+
...options.failure,
|
|
33551
|
+
error: appendSentence(options.failure.error, note)
|
|
33552
|
+
};
|
|
33553
|
+
}
|
|
33554
|
+
async function describeFailureScreenshot(options, deps) {
|
|
33555
|
+
if (options.imageJpegBase64 === undefined) {
|
|
33556
|
+
return interactiveRunnerMessages.actionFailedWithoutScreenshot;
|
|
33557
|
+
}
|
|
33558
|
+
const written = await deps.writeScreenshot({
|
|
33559
|
+
imageJpegBase64: options.imageJpegBase64,
|
|
33560
|
+
path: options.out
|
|
33561
|
+
});
|
|
33562
|
+
if (!written.ok) {
|
|
33563
|
+
return interactiveRunnerMessages.actionFailedScreenshotUnwritten(written.reason === "not-a-jpeg" ? "it did not arrive as a JPEG" : written.detail);
|
|
33564
|
+
}
|
|
33565
|
+
return options.out === stdoutPath ? interactiveRunnerMessages.actionFailedScreenshotToStdout : interactiveRunnerMessages.actionFailedScreenshotWritten(options.out);
|
|
33566
|
+
}
|
|
33567
|
+
|
|
33470
33568
|
// src/core/interactiveRunner/browserAction.ts
|
|
33471
33569
|
function parseJsonPath(path) {
|
|
33472
33570
|
try {
|
|
@@ -33531,30 +33629,56 @@ async function handleRunnerAct(ctx, options, deps) {
|
|
|
33531
33629
|
const built = await readAction(options.type, options.flags, deps);
|
|
33532
33630
|
if (!built.ok)
|
|
33533
33631
|
return { error: built.error, exitCode: exitCodes.invalidArgs };
|
|
33632
|
+
if (options.screenshot === stdoutPath && ctx.outputMode === "human") {
|
|
33633
|
+
return {
|
|
33634
|
+
error: interactiveRunnerMessages.stdoutIsATerminal("--screenshot"),
|
|
33635
|
+
exitCode: exitCodes.invalidArgs
|
|
33636
|
+
};
|
|
33637
|
+
}
|
|
33534
33638
|
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
33535
33639
|
if (resolved.type === "failed") {
|
|
33536
33640
|
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
33537
33641
|
}
|
|
33538
33642
|
announceRunner(ctx, resolved);
|
|
33539
|
-
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, {
|
|
33643
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, {
|
|
33644
|
+
action: built.action,
|
|
33645
|
+
id: resolved.runnerId,
|
|
33646
|
+
...options.screenshot === undefined ? {} : { withScreenshot: true }
|
|
33647
|
+
}, runnerCallOptions);
|
|
33540
33648
|
if (!result.ok) {
|
|
33541
33649
|
const fields = failureFields(result);
|
|
33542
33650
|
return {
|
|
33543
33651
|
...fields,
|
|
33544
33652
|
...result.mayHaveArrived ? {
|
|
33545
|
-
error:
|
|
33653
|
+
error: appendSentence(fields.error, interactiveRunnerMessages.actionMayHaveHappened)
|
|
33546
33654
|
} : {},
|
|
33547
33655
|
exitCode: exitCodes.network
|
|
33548
33656
|
};
|
|
33549
33657
|
}
|
|
33550
|
-
|
|
33658
|
+
const answer = result.value;
|
|
33659
|
+
if (answer.outcome === "success") {
|
|
33660
|
+
if (options.screenshot !== undefined) {
|
|
33661
|
+
return writeActionScreenshot(ctx, {
|
|
33662
|
+
action: built.action,
|
|
33663
|
+
imageJpegBase64: answer.imageJpegBase64,
|
|
33664
|
+
out: options.screenshot
|
|
33665
|
+
}, deps);
|
|
33666
|
+
}
|
|
33551
33667
|
ctx.ui.output({ action: built.action, outcome: "success" }, interactiveRunnerMessages.actionPerformed(built.action.type));
|
|
33552
33668
|
return;
|
|
33553
33669
|
}
|
|
33554
|
-
|
|
33670
|
+
const failure = describePerformActionFailure({
|
|
33555
33671
|
actionType: built.action.type,
|
|
33556
|
-
failure:
|
|
33672
|
+
failure: answer
|
|
33557
33673
|
});
|
|
33674
|
+
if (options.screenshot === undefined || answer.failureReason !== "action-failed") {
|
|
33675
|
+
return failure;
|
|
33676
|
+
}
|
|
33677
|
+
return addFailureScreenshot({
|
|
33678
|
+
failure,
|
|
33679
|
+
imageJpegBase64: answer.imageJpegBase64,
|
|
33680
|
+
out: options.screenshot
|
|
33681
|
+
}, deps);
|
|
33558
33682
|
}
|
|
33559
33683
|
|
|
33560
33684
|
// src/domains/interactiveRunner/takeScreenshot.ts
|
|
@@ -33567,6 +33691,12 @@ async function handleRunnerScreenshot(ctx, options, deps) {
|
|
|
33567
33691
|
if (resolved.type === "failed") {
|
|
33568
33692
|
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
33569
33693
|
}
|
|
33694
|
+
if (options.out === stdoutPath && ctx.outputMode === "human") {
|
|
33695
|
+
return {
|
|
33696
|
+
error: interactiveRunnerMessages.stdoutIsATerminal("--out"),
|
|
33697
|
+
exitCode: exitCodes.invalidArgs
|
|
33698
|
+
};
|
|
33699
|
+
}
|
|
33570
33700
|
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.takeScreenshot, { id: resolved.runnerId }, runnerCallOptions);
|
|
33571
33701
|
if (!result.ok) {
|
|
33572
33702
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
@@ -33576,16 +33706,13 @@ async function handleRunnerScreenshot(ctx, options, deps) {
|
|
|
33576
33706
|
imageJpegBase64: result.value.imageJpegBase64,
|
|
33577
33707
|
path: options.out
|
|
33578
33708
|
});
|
|
33579
|
-
if (!written.ok)
|
|
33580
|
-
return written.
|
|
33581
|
-
|
|
33582
|
-
|
|
33583
|
-
|
|
33584
|
-
|
|
33585
|
-
exitCode: exitCodes.invalidArgs
|
|
33586
|
-
};
|
|
33709
|
+
if (!written.ok)
|
|
33710
|
+
return describeUnwritten(written, options.out);
|
|
33711
|
+
if (options.out === stdoutPath) {
|
|
33712
|
+
ctx.ui.success(interactiveRunnerMessages.screenshotWrittenToStdout);
|
|
33713
|
+
} else {
|
|
33714
|
+
ctx.ui.output({ outcome: "success", path: options.out }, interactiveRunnerMessages.screenshotWritten(options.out));
|
|
33587
33715
|
}
|
|
33588
|
-
ctx.ui.output({ outcome: "success", path: options.out }, interactiveRunnerMessages.screenshotWritten(options.out));
|
|
33589
33716
|
return;
|
|
33590
33717
|
}
|
|
33591
33718
|
const { failureReason } = result.value;
|
|
@@ -33618,13 +33745,27 @@ async function handleRunnerScreenshot(ctx, options, deps) {
|
|
|
33618
33745
|
}
|
|
33619
33746
|
}
|
|
33620
33747
|
}
|
|
33748
|
+
function describeUnwritten(written, out) {
|
|
33749
|
+
if (written.reason === "not-a-jpeg") {
|
|
33750
|
+
return {
|
|
33751
|
+
error: interactiveRunnerMessages.screenshotNotAnImage,
|
|
33752
|
+
exitCode: exitCodes.network
|
|
33753
|
+
};
|
|
33754
|
+
}
|
|
33755
|
+
return {
|
|
33756
|
+
error: out === stdoutPath ? interactiveRunnerMessages.screenshotStdoutUnwritable(written.detail) : interactiveRunnerMessages.screenshotUnwritable(out, written.detail),
|
|
33757
|
+
exitCode: exitCodes.invalidArgs
|
|
33758
|
+
};
|
|
33759
|
+
}
|
|
33621
33760
|
|
|
33622
33761
|
// src/commands/runner/interact.register.ts
|
|
33623
33762
|
var defaultScreenshotPath = "screenshot.jpg";
|
|
33624
33763
|
var screenshotExamples = `
|
|
33625
33764
|
Examples:
|
|
33626
33765
|
$ qawolf runner screenshot
|
|
33627
|
-
$ qawolf runner screenshot --out screens/step-3.jpg
|
|
33766
|
+
$ qawolf runner screenshot --out screens/step-3.jpg
|
|
33767
|
+
$ qawolf runner screenshot --out - > step-3.jpg
|
|
33768
|
+
$ qawolf runner screenshot --out - | my-vision-tool`;
|
|
33628
33769
|
var actExamples = `
|
|
33629
33770
|
Examples:
|
|
33630
33771
|
$ qawolf runner act click --button left --x 480 --y 260
|
|
@@ -33632,15 +33773,12 @@ Examples:
|
|
|
33632
33773
|
$ qawolf runner act keypress --keys Control a
|
|
33633
33774
|
$ qawolf runner act navigate --url https://example.com
|
|
33634
33775
|
$ qawolf runner act drag --path '[{"x":10,"y":20},{"x":80,"y":90}]'
|
|
33635
|
-
$
|
|
33636
|
-
|
|
33637
|
-
|
|
33638
|
-
$ qawolf runner exec snippet.ts
|
|
33639
|
-
$ echo 'console.log(await page.title())' | qawolf runner exec -
|
|
33640
|
-
$ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
|
|
33776
|
+
$ qawolf runner act click --button left --x 480 --y 260 --screenshot step-4.jpg
|
|
33777
|
+
$ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act -
|
|
33778
|
+
$ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act - --screenshot - > step-5.jpg`;
|
|
33641
33779
|
function registerRunnerInteractCommands(runner, signals) {
|
|
33642
|
-
declareCommandKind(runner.command("screenshot"), "read").description("Save a JPEG of an interactive runner's screen to a file").option("--out <path>", "File to write the image to", defaultScreenshotPath).option("--runner <id>", runnerFlagDescription).addHelpText("after", screenshotExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerScreenshot(ctx, { out: opts.out, runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
33643
|
-
declareCommandKind(runner.command("act <action>"), "write").description("Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin. On a mobile runner only click (button left), drag and type have a touchscreen equivalent; the rest answer action-not-supported-on-mobile").option("--button <button>", "click: left, right, wheel, back or forward (mobile: left only)").option("--keys <keys...>", "keypress: modifiers and the key, e.g. Control a").option("--path <json>", "drag: JSON array of points to drag through (mobile: only the first and last are used)").option("--runner <id>", runnerFlagDescription).option("--scroll-x <delta>", "scroll: horizontal wheel delta").option("--scroll-y <delta>", "scroll: vertical wheel delta").option("--text <text>", "type: the text to type").option("--url <url>", "navigate: the http or https URL to go to").option("--x <pixels>", "pointer x, in screenshot pixels").option("--y <pixels>", "pointer y, in screenshot pixels").addHelpText("after", actExamples).action((action, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
|
|
33780
|
+
declareCommandKind(runner.command("screenshot"), "read").description("Save a JPEG of an interactive runner's screen to a file, or write it to stdout with --out -").option("--out <path>", "File to write the image to. - writes the JPEG bytes to stdout on their own and moves the confirmation, JSON included, to stderr", defaultScreenshotPath).option("--runner <id>", runnerFlagDescription).addHelpText("after", screenshotExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerScreenshot(ctx, { out: opts.out, runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
33781
|
+
declareCommandKind(runner.command("act <action>"), "write").description("Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin. On a mobile runner only click (button left), drag and type have a touchscreen equivalent; the rest answer action-not-supported-on-mobile").option("--button <button>", "click: left, right, wheel, back or forward (mobile: left only)").option("--keys <keys...>", "keypress: modifiers and the key, e.g. Control a").option("--path <json>", "drag: JSON array of points to drag through (mobile: only the first and last are used)").option("--runner <id>", runnerFlagDescription).option("--screenshot <path>", "Also save a JPEG of the screen, taken after the action, to this file, in place of a separate screenshot. An action that did not take effect answers with one too. - writes it to stdout and moves the confirmation, JSON included, to stderr").option("--scroll-x <delta>", "scroll: horizontal wheel delta").option("--scroll-y <delta>", "scroll: vertical wheel delta").option("--text <text>", "type: the text to type").option("--url <url>", "navigate: the http or https URL to go to").option("--x <pixels>", "pointer x, in screenshot pixels").option("--y <pixels>", "pointer y, in screenshot pixels").addHelpText("after", actExamples).action((action, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
|
|
33644
33782
|
flags: {
|
|
33645
33783
|
button: opts.button,
|
|
33646
33784
|
keys: opts.keys,
|
|
@@ -33653,9 +33791,9 @@ function registerRunnerInteractCommands(runner, signals) {
|
|
|
33653
33791
|
y: opts.y
|
|
33654
33792
|
},
|
|
33655
33793
|
runner: opts.runner,
|
|
33794
|
+
screenshot: opts.screenshot,
|
|
33656
33795
|
type: action
|
|
33657
33796
|
}, runnerDeps(ctx)))(opts, command));
|
|
33658
|
-
declareCommandKind(runner.command("exec <file>"), "write").description("Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin").option("--file <path>", "File whose scope the snippet is evaluated in; it and the directory's other files travel with it").option("--runner <id>", runnerFlagDescription).addHelpText("after", execExamples).action((file, opts, command) => withAuthContext(signals, (ctx) => handleRunnerExec(ctx, { contextFile: opts.file, runner: opts.runner, source: file }, runnerDeps(ctx)))(opts, command));
|
|
33659
33797
|
}
|
|
33660
33798
|
|
|
33661
33799
|
// src/domains/interactiveRunner/readJournal.ts
|
|
@@ -34610,6 +34748,7 @@ function registerRunnerCommand(program, signals) {
|
|
|
34610
34748
|
registerRunCommand(runner, signals);
|
|
34611
34749
|
registerRunnerEventsCommand(runner, signals);
|
|
34612
34750
|
registerRunnerInteractCommands(runner, signals);
|
|
34751
|
+
registerRunnerExecCommand(runner, signals);
|
|
34613
34752
|
registerRunnerInspectCommands(runner, signals);
|
|
34614
34753
|
registerRunnerImportPackageCommand(runner, signals);
|
|
34615
34754
|
registerRunnerHighlightSelectorCommand(runner, signals);
|
|
@@ -34643,4 +34782,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
34643
34782
|
process.exitCode = 1;
|
|
34644
34783
|
}).finally(() => exitWhenIdle(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
34645
34784
|
|
|
34646
|
-
//# debugId=
|
|
34785
|
+
//# debugId=90417CF807BBB8C264756E2164756E21
|
package/dist/runner-sdk.js
CHANGED
|
@@ -9110,7 +9110,17 @@ var interactMessages = {
|
|
|
9110
9110
|
actionMayHaveHappened: "The runner could not be reached, which does not mean the action was not performed: it may have stopped answering mid-action. Take a screenshot before repeating it.",
|
|
9111
9111
|
actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
|
|
9112
9112
|
actionNotSupportedOnMobile: (type) => `A mobile runner has a touchscreen, so it cannot perform ${type} as asked. It taps with a left-button click, swipes with drag, and types into whatever the last tap focused.`,
|
|
9113
|
+
actionFailedScreenshotToStdout: "Its screen was written to stdout as a JPEG, so look at that rather than sending the action again.",
|
|
9114
|
+
actionFailedScreenshotUnwritten: (detail) => `Its screen could not be written, because ${detail}, so take one with qawolf runner screenshot rather than sending the action again.`,
|
|
9115
|
+
actionFailedScreenshotWritten: (path) => `Its screen was written to ${path}, so look at that rather than sending the action again.`,
|
|
9116
|
+
actionFailedWithoutScreenshot: "The runner answered without a screen, so nothing was written; take one with qawolf runner screenshot rather than sending the action again.",
|
|
9113
9117
|
actionPerformed: (type) => `Performed ${type}.`,
|
|
9118
|
+
actionPerformedScreenshotNotAnImage: (type) => `Performed ${type}, but the screen that came with the answer was not a JPEG, so nothing was written. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead, and report it if it keeps happening.`,
|
|
9119
|
+
actionPerformedScreenshotToStdout: (type) => `Performed ${type} and wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.`,
|
|
9120
|
+
actionPerformedScreenshotStdoutUnwritable: (type, detail) => `Performed ${type}, but its screen could not be written to stdout: ${detail}. The action took effect, so do not repeat it: keep the pipe reading stdout open, or take the screen with qawolf runner screenshot --out <file>.`,
|
|
9121
|
+
actionPerformedScreenshotUnwritable: (type, path, detail) => `Performed ${type}, but its screen could not be written to "${path}": ${detail}. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot, giving --out a path this process can write to.`,
|
|
9122
|
+
actionPerformedScreenshotWritten: (type, path) => `Performed ${type} and wrote the runner's screen to ${path}.`,
|
|
9123
|
+
actionPerformedWithoutScreenshot: (type) => `Performed ${type}, but the runner answered without the screen it was asked for. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead. If this keeps happening, the platform or this CLI is behind the other: upgrade with npm install -g @qawolf/cli.`,
|
|
9114
9124
|
actionAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
9115
9125
|
screenshotAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
9116
9126
|
runnerHasNoScreen: "This runner does not run a browser on a virtual desktop, so there is nothing about it to see or drive. Retrying will never help: launch a playwright runner instead.",
|
|
@@ -9122,8 +9132,11 @@ var interactMessages = {
|
|
|
9122
9132
|
screenNeedsARun: "This runner has not run anything yet, so its screen has never started. Waiting will not clear this and there is nothing to retry: run a flow on it with qawolf runner run, then ask again. Evaluating a snippet does not start a screen.",
|
|
9123
9133
|
screenNotReady: "The runner has a screen and cannot serve this yet. Its virtual desktop restarts when a run changes the display size, and it serves one request at a time, so something already in flight is the usual reason. Retry in a second or two.",
|
|
9124
9134
|
screenshotNotAnImage: "The screen was captured but did not arrive as a JPEG, so nothing was written. Nothing about the command needs changing: try it again, and report it if it keeps happening.",
|
|
9135
|
+
stdoutIsATerminal: (flag) => `Stdout is a terminal, so the JPEG bytes would have nowhere to go. Redirect stdout to a file or pipe it into a reader, or give ${flag} a file path instead of "-".`,
|
|
9136
|
+
screenshotStdoutUnwritable: (detail) => `The screen was captured but could not be written to stdout: ${detail}. Keep the pipe reading stdout open, or give --out a file path instead of "-".`,
|
|
9125
9137
|
screenshotUnwritable: (path, detail) => `The screen was captured but could not be written to "${path}": ${detail}. Give --out a path this process can write to.`,
|
|
9126
9138
|
screenshotWritten: (path) => `Wrote the runner's screen to ${path}.`,
|
|
9139
|
+
screenshotWrittenToStdout: "Wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.",
|
|
9127
9140
|
snippetEmpty: (path) => `"${path}" holds no code to evaluate.`,
|
|
9128
9141
|
snippetErrored: (errorMessage) => errorMessage === undefined ? "The snippet threw and reported no message." : `The snippet threw: ${errorMessage}`,
|
|
9129
9142
|
snippetFileUnreadable: (path) => `Could not read "${path}". Name a readable file, or "-" to read the snippet from stdin.`,
|
|
@@ -10553,19 +10566,38 @@ function isTimeoutError(err) {
|
|
|
10553
10566
|
|
|
10554
10567
|
// src/shell/interactiveRunner/writeScreenshot.ts
|
|
10555
10568
|
var jpegStartOfImage = [255, 216, 255];
|
|
10569
|
+
var stdoutPath = "-";
|
|
10556
10570
|
async function writeScreenshot(options) {
|
|
10557
10571
|
const bytes = Buffer.from(options.imageJpegBase64, "base64");
|
|
10558
10572
|
if (jpegStartOfImage.some((byte, index) => bytes[index] !== byte)) {
|
|
10559
10573
|
return { ok: false, reason: "not-a-jpeg" };
|
|
10560
10574
|
}
|
|
10561
10575
|
try {
|
|
10562
|
-
|
|
10563
|
-
|
|
10576
|
+
if (options.path === stdoutPath) {
|
|
10577
|
+
await writeToStdout(options.stdout ?? process.stdout, bytes);
|
|
10578
|
+
} else {
|
|
10579
|
+
await options.fs.mkdir(dirname3(options.path), { recursive: true });
|
|
10580
|
+
await options.fs.writeFile(options.path, bytes);
|
|
10581
|
+
}
|
|
10564
10582
|
} catch (error) {
|
|
10565
10583
|
return { detail: errorMessage(error), ok: false, reason: "unwritable" };
|
|
10566
10584
|
}
|
|
10567
10585
|
return { ok: true };
|
|
10568
10586
|
}
|
|
10587
|
+
function writeToStdout(stdout, bytes) {
|
|
10588
|
+
return new Promise((resolve, reject) => {
|
|
10589
|
+
const onError = (error) => reject(error);
|
|
10590
|
+
stdout.once("error", onError);
|
|
10591
|
+
stdout.write(bytes, (error) => {
|
|
10592
|
+
if (error) {
|
|
10593
|
+
reject(error);
|
|
10594
|
+
return;
|
|
10595
|
+
}
|
|
10596
|
+
stdout.off("error", onError);
|
|
10597
|
+
resolve();
|
|
10598
|
+
});
|
|
10599
|
+
});
|
|
10600
|
+
}
|
|
10569
10601
|
|
|
10570
10602
|
// src/shell/stdin.ts
|
|
10571
10603
|
async function readStdin() {
|
|
@@ -12575,9 +12607,14 @@ function createPageVerbs({ platformClient }) {
|
|
|
12575
12607
|
return {
|
|
12576
12608
|
async act({
|
|
12577
12609
|
action,
|
|
12578
|
-
runnerId
|
|
12610
|
+
runnerId,
|
|
12611
|
+
withScreenshot
|
|
12579
12612
|
}) {
|
|
12580
|
-
return toSdkResult(await platformClient.callPublicApi(performAction, {
|
|
12613
|
+
return toSdkResult(await platformClient.callPublicApi(performAction, {
|
|
12614
|
+
action,
|
|
12615
|
+
id: runnerId,
|
|
12616
|
+
...withScreenshot === undefined ? {} : { withScreenshot }
|
|
12617
|
+
}, runnerCallOptions));
|
|
12581
12618
|
},
|
|
12582
12619
|
async highlightSelector({
|
|
12583
12620
|
highlight,
|
|
@@ -13177,4 +13214,4 @@ export {
|
|
|
13177
13214
|
createRunnerSdk
|
|
13178
13215
|
};
|
|
13179
13216
|
|
|
13180
|
-
//# debugId=
|
|
13217
|
+
//# debugId=37E313924A61E7F564756E2164756E21
|
|
@@ -10,7 +10,7 @@ export declare function createRunnerSdk(options: RunnerSdkOptions): {
|
|
|
10
10
|
list(): Promise<SdkResult<ListedRunner[]>>;
|
|
11
11
|
evaluateSnippet({ runnerId, scope, source, }: import("./types.js").EvaluateSnippetRequest): Promise<SdkResult<import("./types.js").EvaluatedSnippet>>;
|
|
12
12
|
importPackage({ name, runnerId, version, }: import("./types.js").ImportPackageRequest): Promise<SdkResult<import("./types.js").ImportedPackage>>;
|
|
13
|
-
act({ action, runnerId, }: import("./types.js").ActRequest): Promise<SdkResult<import("./types.js").PerformedAction>>;
|
|
13
|
+
act({ action, runnerId, withScreenshot, }: import("./types.js").ActRequest): Promise<SdkResult<import("./types.js").PerformedAction>>;
|
|
14
14
|
highlightSelector({ highlight, runnerId, }: import("./types.js").HighlightSelectorRequest): Promise<SdkResult<import("./types.js").HighlightedSelector>>;
|
|
15
15
|
inspect({ request, runnerId, }: import("./types.js").InspectRequest): Promise<SdkResult<import("./types.js").Inspected>>;
|
|
16
16
|
promoteSnapshot({ baselinePath, runnerId, screenshotPath, }: import("./types.js").PromoteSnapshotRequest): Promise<SdkResult<import("./types.js").PromotedSnapshot>>;
|
|
@@ -66,6 +66,13 @@ export type EventsRequest = RunnerRequest & {
|
|
|
66
66
|
};
|
|
67
67
|
export type ActRequest = RunnerRequest & {
|
|
68
68
|
action: BrowserAction;
|
|
69
|
+
/**
|
|
70
|
+
* Ask the runner to answer with a screenshot taken after the action, on
|
|
71
|
+
* `imageJpegBase64`. One call instead of an act and a screenshot, with no
|
|
72
|
+
* fixed wait between them. An action that did not take effect answers with
|
|
73
|
+
* one too, and the screen can be absent even when asked for.
|
|
74
|
+
*/
|
|
75
|
+
withScreenshot?: boolean;
|
|
69
76
|
};
|
|
70
77
|
export type EvaluateSnippetRequest = RunnerRequest & {
|
|
71
78
|
scope: SnippetScope;
|
package/package.json
CHANGED
|
@@ -187,7 +187,7 @@ that `url`; never guess a route and never send a repository link in its place.
|
|
|
187
187
|
| `qawolf runner list` | read | List the runners running on your team |
|
|
188
188
|
| `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
|
|
189
189
|
| `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
|
|
190
|
-
| `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |
|
|
190
|
+
| `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file, or write it to stdout with --out - |
|
|
191
191
|
| `qawolf runner stop-run` | write | Stop what a runner is currently executing, leaving the runner up |
|
|
192
192
|
| `qawolf runner terminate` | write | End an interactive runner, and the pod it runs on with it |
|
|
193
193
|
| `qawolf tag create` | write | Create a tag on the caller's team. Tags select flows in run.create. |
|
|
@@ -210,6 +210,12 @@ your actions into Playwright locators), `keepalive` to hold it open, and
|
|
|
210
210
|
when done. Everything is a plain request to one host, so a shell with an API key
|
|
211
211
|
and its own vision model can close the see-and-act loop with no other tooling.
|
|
212
212
|
|
|
213
|
+
When an action is followed by a look at the result, which in a see-and-act loop
|
|
214
|
+
is every action, pass `--screenshot <path>` to `act` (or `-` for stdout) instead
|
|
215
|
+
of calling `act` and then `screenshot`. One call performs the action and writes
|
|
216
|
+
the screen the runner answers with: half the calls per step, and no delay to
|
|
217
|
+
guess at between them.
|
|
218
|
+
|
|
213
219
|
The full workflow is its own guide: how a runner is billed, why the first call
|
|
214
220
|
must be a run, the order the commands go in, the see-and-act loop, `exec`, the
|
|
215
221
|
recorder, reading history, staying alive, and an end-to-end example. **Read
|
|
@@ -127,7 +127,10 @@ vision loop on this surface.
|
|
|
127
127
|
|
|
128
128
|
`qawolf runner screenshot --out page.jpg` writes a real JPEG to disk, decoded,
|
|
129
129
|
because every coding harness can open an image file. Read it with whatever
|
|
130
|
-
vision you have.
|
|
130
|
+
vision you have. `--out -` writes the JPEG bytes to stdout instead, on their own,
|
|
131
|
+
for a caller that is a process rather than an agent: the confirmation, and the
|
|
132
|
+
JSON line under `--json`, goes to stderr so nothing follows the image on stdout.
|
|
133
|
+
A terminal on stdout is refused: redirect or pipe it.
|
|
131
134
|
|
|
132
135
|
`qawolf runner act <action>` performs exactly one action per call, in the
|
|
133
136
|
computer-use tool vocabulary a vision model already emits: `click`,
|
|
@@ -139,6 +142,19 @@ can forward a tool call rather than translate it:
|
|
|
139
142
|
echo '{"type":"click","button":"left","x":480,"y":260}' | qawolf runner act -
|
|
140
143
|
```
|
|
141
144
|
|
|
145
|
+
Every action in a see-and-act loop is followed by a look at the result, so ask
|
|
146
|
+
for it in the same call: `act ... --screenshot step-1.jpg` (or `--screenshot -`
|
|
147
|
+
for stdout) performs the action and writes the screen the runner answers with.
|
|
148
|
+
Prefer it over `act` and then `screenshot`: each step is one call instead of
|
|
149
|
+
two, with no delay to guess at between them. The screen the runner answers with
|
|
150
|
+
is taken once it has changed from just before the action or half a second has
|
|
151
|
+
passed, whichever comes first. An action that reached the screen and did not
|
|
152
|
+
take effect answers with one too, so a `1` from `act --screenshot` still leaves
|
|
153
|
+
you a picture of why the click missed. A `4` whose message starts with
|
|
154
|
+
"Performed" means the action happened and only the picture is missing: take a
|
|
155
|
+
`screenshot`, never send the action again to get it. As with
|
|
156
|
+
`screenshot --out -`, a terminal on stdout is refused.
|
|
157
|
+
|
|
142
158
|
Coordinates are pixels on the same screenshot you just read. The runner serves
|
|
143
159
|
one see-or-act request at a time, so decide what to do next from each answer
|
|
144
160
|
rather than firing several. Bounds are checked before anything is sent, so an
|
|
@@ -565,10 +581,9 @@ export QAWOLF_RUNNER_ID=agent-1 # so no command below needs --runner
|
|
|
565
581
|
qawolf runner launch --id agent-1 --json # --id, not the variable; read .alreadyRunning
|
|
566
582
|
qawolf runner run flows/smoke.flow.ts --follow # starts the screen; exit 1 if it failed
|
|
567
583
|
|
|
568
|
-
qawolf runner act navigate --url https://example.com/login
|
|
569
|
-
qawolf runner
|
|
570
|
-
qawolf runner act
|
|
571
|
-
qawolf runner act type --text "someone@example.com"
|
|
584
|
+
qawolf runner act navigate --url https://example.com/login --screenshot step-1.jpg # then read step-1.jpg yourself
|
|
585
|
+
qawolf runner act click --button left --x 480 --y 260 --screenshot step-2.jpg
|
|
586
|
+
qawolf runner act type --text "someone@example.com" --screenshot step-3.jpg
|
|
572
587
|
|
|
573
588
|
qawolf runner inspect element-html --selector "#email"
|
|
574
589
|
qawolf runner inspect variable --name cart | jq .total
|