@qawolf/cli 1.18.0 → 1.19.1
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 +254 -52
- package/package.json +1 -1
- package/skills/qawolf-cli/SKILL.md +5 -1
- package/skills/qawolf-cli/references/runner.md +63 -0
package/dist/cli.js
CHANGED
|
@@ -325822,6 +325822,7 @@ var authMessages = {
|
|
|
325822
325822
|
},
|
|
325823
325823
|
request: {
|
|
325824
325824
|
rejected401: (noun) => `QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 401). Check your API key.`,
|
|
325825
|
+
rejected402: (noun) => `QA Wolf API refused the${noun ? ` ${noun}` : ""} request (HTTP 402): billing prevented it.`,
|
|
325825
325826
|
rejected403: (noun) => `QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 403). Check that your API key has access to this environment.`,
|
|
325826
325827
|
notFound404: (noun) => `QA Wolf API could not find ${noun ? `${noun} for that environment` : "that environment"} (HTTP 404). Check the --env value.`,
|
|
325827
325828
|
failedWithStatus: (status, noun) => `QA Wolf API${noun ? ` ${noun}` : ""} request failed (HTTP ${status}).`,
|
|
@@ -326107,6 +326108,8 @@ var interactMessages = {
|
|
|
326107
326108
|
inspectNeedsABrowserRunner: "This runner is a mobile device, and element and page HTML are a browser's shapes. Retrying will never help. Launch a playwright runner to inspect one.",
|
|
326108
326109
|
nothingToInspect: (errorMessage2) => `The runner had nothing to inspect${errorMessage2 === undefined ? "" : `: ${errorMessage2}`}. There is no live page, nothing matched the selector, or no variable has that name; a runner cannot tell those apart. Run a flow on it first, or check the selector or name.`,
|
|
326109
326110
|
inspectAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
326111
|
+
inspectMobileAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
326112
|
+
runnerIsNotMobile: "This runner is not a mobile device, so there is nothing here to inspect. Retrying will never help: launch an android or ios runner instead.",
|
|
326110
326113
|
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.",
|
|
326111
326114
|
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.",
|
|
326112
326115
|
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.",
|
|
@@ -326143,6 +326146,7 @@ var lifecycleMessages = {
|
|
|
326143
326146
|
alreadyRunning: (id) => `Runner ${id} was already running.`,
|
|
326144
326147
|
defaultNotRemembered: (id) => `Runner ${id} could not be written to .qawolf as this directory's default, so later commands will not find it on their own. Pass --runner ${id}, or set QAWOLF_RUNNER_ID=${id}.`,
|
|
326145
326148
|
defaultNotForgotten: (id) => `Runner ${id} was terminated, but it could not be removed from .qawolf as this directory's default, so later commands will still be sent to it. Pass --runner, or launch a new runner.`,
|
|
326149
|
+
envRunnerIdShadowsLaunch: (launchedId, envId) => `QAWOLF_RUNNER_ID=${envId} is still set, so commands that omit --runner will keep targeting it instead of the runner just launched. Pass --runner ${launchedId}, or export QAWOLF_RUNNER_ID=${launchedId}.`,
|
|
326146
326150
|
keptAlive: (id) => `Runner ${id} is alive, and its inactivity clock has been reset.`,
|
|
326147
326151
|
launchFailed: (id, error) => `Launching runner ${id} failed. ${error}`,
|
|
326148
326152
|
launchLost: (id, error) => `Launching runner ${id} failed. ${error}. The request may still have reached QA Wolf and started a runner, so relaunch the same id with qawolf runner launch --id ${id} to attach to it rather than start a second one, or terminate it with qawolf runner terminate --runner ${id}.`,
|
|
@@ -326379,7 +326383,8 @@ var exitCodes = {
|
|
|
326379
326383
|
auth: 3,
|
|
326380
326384
|
network: 4,
|
|
326381
326385
|
config: 5,
|
|
326382
|
-
timeout: 6
|
|
326386
|
+
timeout: 6,
|
|
326387
|
+
payment: 7
|
|
326383
326388
|
};
|
|
326384
326389
|
function exit(code, message, proc = process) {
|
|
326385
326390
|
if (message !== undefined && message.length > 0) {
|
|
@@ -341063,6 +341068,12 @@ function describeRequestError(err, baseUrl, noun) {
|
|
|
341063
341068
|
exitCode: exitCodes.auth,
|
|
341064
341069
|
...body
|
|
341065
341070
|
};
|
|
341071
|
+
if (err.status === 402)
|
|
341072
|
+
return {
|
|
341073
|
+
error: m.request.rejected402(noun),
|
|
341074
|
+
exitCode: exitCodes.payment,
|
|
341075
|
+
...body
|
|
341076
|
+
};
|
|
341066
341077
|
if (err.status === 403)
|
|
341067
341078
|
return { error: m.request.rejected403(noun), ...body };
|
|
341068
341079
|
if (err.status === 404)
|
|
@@ -342551,7 +342562,6 @@ var makeInspectMobileOnRunnerContract = () => {
|
|
|
342551
342562
|
output
|
|
342552
342563
|
};
|
|
342553
342564
|
};
|
|
342554
|
-
|
|
342555
342565
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/journal.js
|
|
342556
342566
|
var journalEntrySchema = exports_external.object({
|
|
342557
342567
|
payload: exports_external.unknown(),
|
|
@@ -346126,7 +346136,7 @@ function startUpdateCheck(deps) {
|
|
|
346126
346136
|
// package.json
|
|
346127
346137
|
var package_default = {
|
|
346128
346138
|
name: "@qawolf/cli",
|
|
346129
|
-
version: "1.
|
|
346139
|
+
version: "1.19.1",
|
|
346130
346140
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
346131
346141
|
keywords: [
|
|
346132
346142
|
"automation",
|
|
@@ -356706,7 +356716,7 @@ async function launchRunner(ctx, options) {
|
|
|
356706
356716
|
if (!result.ok) {
|
|
356707
356717
|
return {
|
|
356708
356718
|
...failureFields(result),
|
|
356709
|
-
exitCode: exitCodes.network,
|
|
356719
|
+
exitCode: result.exitCode ?? exitCodes.network,
|
|
356710
356720
|
mayHaveArrived: result.mayHaveArrived ?? false,
|
|
356711
356721
|
ok: false
|
|
356712
356722
|
};
|
|
@@ -357346,7 +357356,10 @@ async function handleRunnerImportPackage(ctx, options, deps) {
|
|
|
357346
357356
|
packageVersion
|
|
357347
357357
|
}, runnerCallOptions);
|
|
357348
357358
|
if (!result.ok) {
|
|
357349
|
-
return {
|
|
357359
|
+
return {
|
|
357360
|
+
...failureFields(result),
|
|
357361
|
+
exitCode: result.exitCode ?? exitCodes.network
|
|
357362
|
+
};
|
|
357350
357363
|
}
|
|
357351
357364
|
if (result.value.outcome === "failure") {
|
|
357352
357365
|
const failure = result.value;
|
|
@@ -357446,14 +357459,190 @@ async function handleRunnerInspect(ctx, options, deps) {
|
|
|
357446
357459
|
return;
|
|
357447
357460
|
}
|
|
357448
357461
|
|
|
357462
|
+
// src/core/interactiveRunner/inspectMobileRequest.ts
|
|
357463
|
+
function toNumber(value) {
|
|
357464
|
+
return value.trim() === "" ? Number.NaN : Number(value);
|
|
357465
|
+
}
|
|
357466
|
+
function buildInspectMobileRequest(what, flags) {
|
|
357467
|
+
if (flags.by === "point" && (flags.text !== undefined || flags.partial !== undefined)) {
|
|
357468
|
+
return {
|
|
357469
|
+
error: "--by point matches by pixel, so --text/--partial would be ignored rather than searching by text. Pass --by text instead, or drop --text/--partial.",
|
|
357470
|
+
ok: false
|
|
357471
|
+
};
|
|
357472
|
+
}
|
|
357473
|
+
if (flags.by === "text" && (flags.x !== undefined || flags.y !== undefined)) {
|
|
357474
|
+
return {
|
|
357475
|
+
error: "--by text matches by text, so --x/--y would be ignored rather than matching a point. Pass --by point instead, or drop --x/--y.",
|
|
357476
|
+
ok: false
|
|
357477
|
+
};
|
|
357478
|
+
}
|
|
357479
|
+
const candidate = {
|
|
357480
|
+
what,
|
|
357481
|
+
...flags.by === undefined ? {} : { by: flags.by },
|
|
357482
|
+
...flags.context === undefined ? {} : { context: flags.context },
|
|
357483
|
+
...flags.partial === undefined ? {} : { partial: flags.partial },
|
|
357484
|
+
...flags.text === undefined ? {} : { text: flags.text },
|
|
357485
|
+
...flags.x === undefined ? {} : { x: toNumber(flags.x) },
|
|
357486
|
+
...flags.y === undefined ? {} : { y: toNumber(flags.y) }
|
|
357487
|
+
};
|
|
357488
|
+
const parsed = inspectMobileRequestSchema.safeParse(candidate);
|
|
357489
|
+
if (!parsed.success) {
|
|
357490
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
357491
|
+
}
|
|
357492
|
+
return { ok: true, request: parsed.data };
|
|
357493
|
+
}
|
|
357494
|
+
|
|
357495
|
+
// src/domains/interactiveRunner/inspectMobile.ts
|
|
357496
|
+
function describeSession(session) {
|
|
357497
|
+
switch (session.type) {
|
|
357498
|
+
case "ready":
|
|
357499
|
+
return session.deviceName === undefined ? `Session ready: ${session.platformName} (${session.sessionId}).` : `Session ready: ${session.platformName} on ${session.deviceName} (${session.sessionId}).`;
|
|
357500
|
+
case "unreachable":
|
|
357501
|
+
return `Session unreachable: ${session.error}`;
|
|
357502
|
+
case "ambiguous":
|
|
357503
|
+
return `${String(session.sessionCount)} Appium sessions are live; expected one.`;
|
|
357504
|
+
case "no-session":
|
|
357505
|
+
return "No Appium session is live.";
|
|
357506
|
+
}
|
|
357507
|
+
}
|
|
357508
|
+
function streamLine(value) {
|
|
357509
|
+
switch (value.what) {
|
|
357510
|
+
case "contexts":
|
|
357511
|
+
return JSON.stringify({
|
|
357512
|
+
contexts: value.contexts,
|
|
357513
|
+
current: value.current
|
|
357514
|
+
});
|
|
357515
|
+
case "page":
|
|
357516
|
+
return JSON.stringify({
|
|
357517
|
+
context: value.context,
|
|
357518
|
+
orientation: value.orientation,
|
|
357519
|
+
pageSource: value.pageSource
|
|
357520
|
+
});
|
|
357521
|
+
case "elements":
|
|
357522
|
+
return JSON.stringify({ matches: value.matches });
|
|
357523
|
+
}
|
|
357524
|
+
}
|
|
357525
|
+
async function handleRunnerInspectMobile(ctx, options, deps) {
|
|
357526
|
+
const built = buildInspectMobileRequest(options.what, options.flags);
|
|
357527
|
+
if (!built.ok)
|
|
357528
|
+
return { error: built.error, exitCode: exitCodes.invalidArgs };
|
|
357529
|
+
const resolved = await resolveRunner(ctx, {
|
|
357530
|
+
autoLaunch: false,
|
|
357531
|
+
noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForInspect,
|
|
357532
|
+
runner: options.runner
|
|
357533
|
+
}, deps);
|
|
357534
|
+
if (resolved.type === "failed") {
|
|
357535
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
357536
|
+
}
|
|
357537
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.inspectMobile, { id: resolved.runnerId, request: built.request }, runnerCallOptions);
|
|
357538
|
+
if (!result.ok) {
|
|
357539
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
357540
|
+
}
|
|
357541
|
+
if (result.value.outcome === "success") {
|
|
357542
|
+
const value = result.value;
|
|
357543
|
+
if (value.what === "session") {
|
|
357544
|
+
ctx.ui.output(value, describeSession(value.session));
|
|
357545
|
+
} else {
|
|
357546
|
+
ctx.ui.stream(value, streamLine(value));
|
|
357547
|
+
}
|
|
357548
|
+
return;
|
|
357549
|
+
}
|
|
357550
|
+
const { failureReason } = result.value;
|
|
357551
|
+
switch (failureReason) {
|
|
357552
|
+
case "runner-is-not-mobile":
|
|
357553
|
+
return {
|
|
357554
|
+
error: interactiveRunnerMessages.runnerIsNotMobile,
|
|
357555
|
+
exitCode: exitCodes.invalidArgs
|
|
357556
|
+
};
|
|
357557
|
+
case "screen-needs-a-run":
|
|
357558
|
+
return {
|
|
357559
|
+
error: interactiveRunnerMessages.screenNeedsARun,
|
|
357560
|
+
exitCode: exitCodes.invalidArgs
|
|
357561
|
+
};
|
|
357562
|
+
case "screen-not-ready":
|
|
357563
|
+
return {
|
|
357564
|
+
error: interactiveRunnerMessages.screenNotReady,
|
|
357565
|
+
exitCode: exitCodes.network
|
|
357566
|
+
};
|
|
357567
|
+
case "runner-unreachable":
|
|
357568
|
+
return {
|
|
357569
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
357570
|
+
exitCode: exitCodes.network
|
|
357571
|
+
};
|
|
357572
|
+
default: {
|
|
357573
|
+
return {
|
|
357574
|
+
error: interactiveRunnerMessages.inspectMobileAnsweredUnknown(failureReason),
|
|
357575
|
+
exitCode: exitCodes.network
|
|
357576
|
+
};
|
|
357577
|
+
}
|
|
357578
|
+
}
|
|
357579
|
+
}
|
|
357580
|
+
|
|
357581
|
+
// src/commands/runner/inspectMobile.register.ts
|
|
357582
|
+
function registerRunnerInspectMobileCommands(inspect, signals) {
|
|
357583
|
+
declareCommandKind(inspect.command("session"), "read").description("Print the Appium session's status: ready, or why not").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357584
|
+
flags: {
|
|
357585
|
+
by: undefined,
|
|
357586
|
+
context: undefined,
|
|
357587
|
+
partial: undefined,
|
|
357588
|
+
text: undefined,
|
|
357589
|
+
x: undefined,
|
|
357590
|
+
y: undefined
|
|
357591
|
+
},
|
|
357592
|
+
runner: opts.runner,
|
|
357593
|
+
what: "session"
|
|
357594
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357595
|
+
declareCommandKind(inspect.command("contexts"), "read").description("List the WebView contexts available, and which is current").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357596
|
+
flags: {
|
|
357597
|
+
by: undefined,
|
|
357598
|
+
context: undefined,
|
|
357599
|
+
partial: undefined,
|
|
357600
|
+
text: undefined,
|
|
357601
|
+
x: undefined,
|
|
357602
|
+
y: undefined
|
|
357603
|
+
},
|
|
357604
|
+
runner: opts.runner,
|
|
357605
|
+
what: "contexts"
|
|
357606
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357607
|
+
declareCommandKind(inspect.command("page-source"), "read").description("Print the current context's page source, as a tree").option("--context <name>", "Read this context instead of the current one").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357608
|
+
flags: {
|
|
357609
|
+
by: undefined,
|
|
357610
|
+
context: opts.context,
|
|
357611
|
+
partial: undefined,
|
|
357612
|
+
text: undefined,
|
|
357613
|
+
x: undefined,
|
|
357614
|
+
y: undefined
|
|
357615
|
+
},
|
|
357616
|
+
runner: opts.runner,
|
|
357617
|
+
what: "page"
|
|
357618
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357619
|
+
declareCommandKind(inspect.command("elements"), "read").description("Find elements at a screen point, or elements carrying some text").requiredOption("--by <by>", "point or text").option("--context <name>", "Read this context instead of the current one").option("--partial", "text: match text containing this, rather than exactly this").option("--runner <id>", runnerFlagDescription).option("--text <text>", "text: the text to match").option("--x <pixels>", "point: whole pixels on the device's own screen").option("--y <pixels>", "point: whole pixels on the device's own screen").action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357620
|
+
flags: {
|
|
357621
|
+
by: opts.by,
|
|
357622
|
+
context: opts.context,
|
|
357623
|
+
partial: opts.partial,
|
|
357624
|
+
text: opts.text,
|
|
357625
|
+
x: opts.x,
|
|
357626
|
+
y: opts.y
|
|
357627
|
+
},
|
|
357628
|
+
runner: opts.runner,
|
|
357629
|
+
what: "elements"
|
|
357630
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357631
|
+
}
|
|
357632
|
+
|
|
357449
357633
|
// src/commands/runner/inspect.register.ts
|
|
357450
357634
|
var inspectExamples = `
|
|
357451
357635
|
Examples:
|
|
357452
357636
|
$ qawolf runner inspect element-html --selector "#email"
|
|
357453
357637
|
$ qawolf runner inspect page-html > page.html
|
|
357454
|
-
$ qawolf runner inspect variable --name cart | jq .total
|
|
357638
|
+
$ qawolf runner inspect variable --name cart | jq .total
|
|
357639
|
+
$ qawolf runner inspect session
|
|
357640
|
+
$ qawolf runner inspect contexts
|
|
357641
|
+
$ qawolf runner inspect page-source --context WEBVIEW_1
|
|
357642
|
+
$ qawolf runner inspect elements --by point --x 200 --y 400
|
|
357643
|
+
$ qawolf runner inspect elements --by text --text "Sign in" --partial`;
|
|
357455
357644
|
function registerRunnerInspectCommands(runner, signals) {
|
|
357456
|
-
const inspect = runner.command("inspect").description("Read one thing off a runner's live page").addHelpText("after", inspectExamples);
|
|
357645
|
+
const inspect = runner.command("inspect").description("Read one thing off a runner's live page (browser) or Appium session (mobile)").addHelpText("after", inspectExamples);
|
|
357457
357646
|
declareCommandKind(inspect.command("element-html"), "read").description("Print the HTML of the first element a selector matches").requiredOption("--selector <selector>", "Playwright selector to inspect").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspect(ctx, {
|
|
357458
357647
|
flags: { name: undefined, selector: opts.selector },
|
|
357459
357648
|
runner: opts.runner,
|
|
@@ -357469,6 +357658,7 @@ function registerRunnerInspectCommands(runner, signals) {
|
|
|
357469
357658
|
runner: opts.runner,
|
|
357470
357659
|
what: "variable"
|
|
357471
357660
|
}, runnerDeps(ctx)))(opts, command));
|
|
357661
|
+
registerRunnerInspectMobileCommands(inspect, signals);
|
|
357472
357662
|
}
|
|
357473
357663
|
|
|
357474
357664
|
// src/core/interactiveRunner/runFiles.ts
|
|
@@ -357580,7 +357770,10 @@ async function handleRunnerExec(ctx, options, deps) {
|
|
|
357580
357770
|
...scope.files === undefined ? {} : { files: scope.files }
|
|
357581
357771
|
}, runnerCallOptions);
|
|
357582
357772
|
if (!result.ok) {
|
|
357583
|
-
return {
|
|
357773
|
+
return {
|
|
357774
|
+
...failureFields(result),
|
|
357775
|
+
exitCode: result.exitCode ?? exitCodes.network
|
|
357776
|
+
};
|
|
357584
357777
|
}
|
|
357585
357778
|
if (result.value.outcome === "failure") {
|
|
357586
357779
|
result.value.failureReason;
|
|
@@ -357606,46 +357799,6 @@ async function handleRunnerExec(ctx, options, deps) {
|
|
|
357606
357799
|
}
|
|
357607
357800
|
}
|
|
357608
357801
|
|
|
357609
|
-
// src/core/interactiveRunner/browserAction.ts
|
|
357610
|
-
function parseJsonPath(path9) {
|
|
357611
|
-
try {
|
|
357612
|
-
return { ok: true, value: JSON.parse(path9) };
|
|
357613
|
-
} catch {
|
|
357614
|
-
return {
|
|
357615
|
-
error: `--path must be a JSON array of points, for example '[{"x":10,"y":20},{"x":80,"y":90}]'.`,
|
|
357616
|
-
ok: false
|
|
357617
|
-
};
|
|
357618
|
-
}
|
|
357619
|
-
}
|
|
357620
|
-
function toNumber(value) {
|
|
357621
|
-
return value.trim() === "" ? Number.NaN : Number(value);
|
|
357622
|
-
}
|
|
357623
|
-
function buildBrowserAction(type, flags) {
|
|
357624
|
-
const parsedPath = flags.path === undefined ? undefined : parseJsonPath(flags.path);
|
|
357625
|
-
if (parsedPath !== undefined && !parsedPath.ok)
|
|
357626
|
-
return parsedPath;
|
|
357627
|
-
const candidate = {
|
|
357628
|
-
type,
|
|
357629
|
-
...flags.button === undefined ? {} : { button: flags.button },
|
|
357630
|
-
...flags.keys === undefined ? {} : { keys: flags.keys },
|
|
357631
|
-
...parsedPath === undefined ? {} : { path: parsedPath.value },
|
|
357632
|
-
...flags.scrollX === undefined ? {} : { scroll_x: toNumber(flags.scrollX) },
|
|
357633
|
-
...flags.scrollY === undefined ? {} : { scroll_y: toNumber(flags.scrollY) },
|
|
357634
|
-
...flags.text === undefined ? {} : { text: flags.text },
|
|
357635
|
-
...flags.url === undefined ? {} : { url: flags.url },
|
|
357636
|
-
...flags.x === undefined ? {} : { x: toNumber(flags.x) },
|
|
357637
|
-
...flags.y === undefined ? {} : { y: toNumber(flags.y) }
|
|
357638
|
-
};
|
|
357639
|
-
return parseBrowserAction(candidate);
|
|
357640
|
-
}
|
|
357641
|
-
function parseBrowserAction(candidate) {
|
|
357642
|
-
const parsed = browserActionSchema.safeParse(candidate);
|
|
357643
|
-
if (!parsed.success) {
|
|
357644
|
-
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
357645
|
-
}
|
|
357646
|
-
return { action: parsed.data, ok: true };
|
|
357647
|
-
}
|
|
357648
|
-
|
|
357649
357802
|
// src/domains/interactiveRunner/performActionFailure.ts
|
|
357650
357803
|
function describePerformActionFailure(options) {
|
|
357651
357804
|
const { actionType, failure } = options;
|
|
@@ -357690,7 +357843,47 @@ function describePerformActionFailure(options) {
|
|
|
357690
357843
|
}
|
|
357691
357844
|
}
|
|
357692
357845
|
|
|
357693
|
-
// src/
|
|
357846
|
+
// src/core/interactiveRunner/browserAction.ts
|
|
357847
|
+
function parseJsonPath(path9) {
|
|
357848
|
+
try {
|
|
357849
|
+
return { ok: true, value: JSON.parse(path9) };
|
|
357850
|
+
} catch {
|
|
357851
|
+
return {
|
|
357852
|
+
error: `--path must be a JSON array of points, for example '[{"x":10,"y":20},{"x":80,"y":90}]'.`,
|
|
357853
|
+
ok: false
|
|
357854
|
+
};
|
|
357855
|
+
}
|
|
357856
|
+
}
|
|
357857
|
+
function toNumber2(value) {
|
|
357858
|
+
return value.trim() === "" ? Number.NaN : Number(value);
|
|
357859
|
+
}
|
|
357860
|
+
function buildBrowserAction(type, flags) {
|
|
357861
|
+
const parsedPath = flags.path === undefined ? undefined : parseJsonPath(flags.path);
|
|
357862
|
+
if (parsedPath !== undefined && !parsedPath.ok)
|
|
357863
|
+
return parsedPath;
|
|
357864
|
+
const candidate = {
|
|
357865
|
+
type,
|
|
357866
|
+
...flags.button === undefined ? {} : { button: flags.button },
|
|
357867
|
+
...flags.keys === undefined ? {} : { keys: flags.keys },
|
|
357868
|
+
...parsedPath === undefined ? {} : { path: parsedPath.value },
|
|
357869
|
+
...flags.scrollX === undefined ? {} : { scroll_x: toNumber2(flags.scrollX) },
|
|
357870
|
+
...flags.scrollY === undefined ? {} : { scroll_y: toNumber2(flags.scrollY) },
|
|
357871
|
+
...flags.text === undefined ? {} : { text: flags.text },
|
|
357872
|
+
...flags.url === undefined ? {} : { url: flags.url },
|
|
357873
|
+
...flags.x === undefined ? {} : { x: toNumber2(flags.x) },
|
|
357874
|
+
...flags.y === undefined ? {} : { y: toNumber2(flags.y) }
|
|
357875
|
+
};
|
|
357876
|
+
return parseBrowserAction(candidate);
|
|
357877
|
+
}
|
|
357878
|
+
function parseBrowserAction(candidate) {
|
|
357879
|
+
const parsed = browserActionSchema.safeParse(candidate);
|
|
357880
|
+
if (!parsed.success) {
|
|
357881
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
357882
|
+
}
|
|
357883
|
+
return { action: parsed.data, ok: true };
|
|
357884
|
+
}
|
|
357885
|
+
|
|
357886
|
+
// src/domains/interactiveRunner/readAction.ts
|
|
357694
357887
|
var stdinArgument2 = "-";
|
|
357695
357888
|
async function readAction(type, flags, deps) {
|
|
357696
357889
|
if (type !== stdinArgument2)
|
|
@@ -357708,6 +357901,8 @@ async function readAction(type, flags, deps) {
|
|
|
357708
357901
|
return { error: interactiveRunnerMessages.actionNotJson, ok: false };
|
|
357709
357902
|
}
|
|
357710
357903
|
}
|
|
357904
|
+
|
|
357905
|
+
// src/domains/interactiveRunner/performAction.ts
|
|
357711
357906
|
async function handleRunnerAct(ctx, options, deps) {
|
|
357712
357907
|
const built = await readAction(options.type, options.flags, deps);
|
|
357713
357908
|
if (!built.ok)
|
|
@@ -357821,7 +358016,7 @@ Examples:
|
|
|
357821
358016
|
$ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
|
|
357822
358017
|
function registerRunnerInteractCommands(runner, signals) {
|
|
357823
358018
|
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));
|
|
357824
|
-
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").option("--button <button>", "click: left, right, wheel, back or forward").option("--keys <keys...>", "keypress: modifiers and the key, e.g. Control a").option("--path <json>", "drag: JSON array of points to drag through").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((action2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
|
|
358019
|
+
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((action2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
|
|
357825
358020
|
flags: {
|
|
357826
358021
|
button: opts.button,
|
|
357827
358022
|
keys: opts.keys,
|
|
@@ -357902,6 +358097,10 @@ async function handleRunnerLaunch(ctx, options, deps) {
|
|
|
357902
358097
|
if (!launched.ok) {
|
|
357903
358098
|
return { ...failureFields(launched), exitCode: launched.exitCode };
|
|
357904
358099
|
}
|
|
358100
|
+
const envRunnerId = deps.env[runnerIdEnvironmentVariable]?.trim();
|
|
358101
|
+
if (envRunnerId && envRunnerId !== launched.value.id) {
|
|
358102
|
+
ctx.ui.warn(interactiveRunnerMessages.envRunnerIdShadowsLaunch(launched.value.id, envRunnerId));
|
|
358103
|
+
}
|
|
357905
358104
|
ctx.ui.output(launched.value, launched.value.alreadyRunning ? interactiveRunnerMessages.alreadyRunning(launched.value.id) : interactiveRunnerMessages.launched(launched.value.id));
|
|
357906
358105
|
return;
|
|
357907
358106
|
}
|
|
@@ -358616,7 +358815,10 @@ async function sendRunFlowRequest(ctx, options, delta) {
|
|
|
358616
358815
|
}, runnerCallOptions);
|
|
358617
358816
|
if (!result.ok) {
|
|
358618
358817
|
return {
|
|
358619
|
-
failure: {
|
|
358818
|
+
failure: {
|
|
358819
|
+
...failureFields(result),
|
|
358820
|
+
exitCode: result.exitCode ?? exitCodes.network
|
|
358821
|
+
},
|
|
358620
358822
|
type: "failed"
|
|
358621
358823
|
};
|
|
358622
358824
|
}
|
|
@@ -358828,4 +359030,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
358828
359030
|
process.exitCode = 1;
|
|
358829
359031
|
}).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
358830
359032
|
|
|
358831
|
-
//# debugId=
|
|
359033
|
+
//# debugId=F98A1DC596D4CF4664756E2164756E21
|
package/package.json
CHANGED
|
@@ -155,13 +155,17 @@ that `url`; never guess a route and never send a repository link in its place.
|
|
|
155
155
|
| `qawolf run find` | read | List an environment's recent runs, newest first. |
|
|
156
156
|
| `qawolf run get` | read | Get a run's status, per-flow results, and links. |
|
|
157
157
|
| `qawolf run reattempt` | write | Request new attempts for a run's flows, in the same run. A flow is eligible once its result is failed or canceled and QA Wolf's automatic retries have finished. A fully investigated run no longer accepts reattempts. Attempts run with the latest flow code. Poll run.get for results. |
|
|
158
|
-
| `qawolf runner act` | write | 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 |
|
|
158
|
+
| `qawolf runner act` | write | 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 |
|
|
159
159
|
| `qawolf runner events` | read | Print a runner's journal, one entry per line. QA Wolf writes console, recorder, run-events, run-logs, run-status |
|
|
160
160
|
| `qawolf runner exec` | write | Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin |
|
|
161
161
|
| `qawolf runner highlight-selector` | write | Highlight what a selector matches on a runner's live page, so the next screenshot shows it. Omit the selector to clear the highlight |
|
|
162
162
|
| `qawolf runner import-package` | write | Install a package into a runner's live run, so a snippet or a selection can import it |
|
|
163
|
+
| `qawolf runner inspect contexts` | read | List the WebView contexts available, and which is current |
|
|
163
164
|
| `qawolf runner inspect element-html` | read | Print the HTML of the first element a selector matches |
|
|
165
|
+
| `qawolf runner inspect elements` | read | Find elements at a screen point, or elements carrying some text |
|
|
164
166
|
| `qawolf runner inspect page-html` | read | Print the page's HTML, simplified for a model to read |
|
|
167
|
+
| `qawolf runner inspect page-source` | read | Print the current context's page source, as a tree |
|
|
168
|
+
| `qawolf runner inspect session` | read | Print the Appium session's status: ready, or why not |
|
|
165
169
|
| `qawolf runner inspect variable` | read | Print a top-level variable's value from the running workflow |
|
|
166
170
|
| `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
|
|
167
171
|
| `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
|
|
@@ -41,6 +41,15 @@ No `read` command ever launches a runner. `screenshot`, `events` and `keepalive`
|
|
|
41
41
|
tell you there is no runner rather than quietly billing one, and so does
|
|
42
42
|
`terminate`, since starting a pod in order to end it would be absurd.
|
|
43
43
|
|
|
44
|
+
`--name` also chooses between a browser and a mobile device:
|
|
45
|
+
`qawolf runner launch --name android` or `--name ios` starts an Appium session
|
|
46
|
+
instead of a browser. A command built for the other kind answers a
|
|
47
|
+
`failureReason` naming the mismatch rather than doing something approximate —
|
|
48
|
+
`runner-is-not-mobile` from `inspect session`/`contexts`/`page-source`/`elements`
|
|
49
|
+
on a browser runner, `runner-is-not-a-browser` from `inspect element-html`/
|
|
50
|
+
`page-html` on a mobile one — so launch the right family up front rather than
|
|
51
|
+
discovering it from a refusal mid-session.
|
|
52
|
+
|
|
44
53
|
## Knowing what you are holding
|
|
45
54
|
|
|
46
55
|
`qawolf runner list` names the runners this directory has launched that are
|
|
@@ -138,6 +147,14 @@ taken effect. On a `4` from `act`, take a screenshot before repeating a click.
|
|
|
138
147
|
looks the same from outside, so treat a `4` from a snippet that changes something
|
|
139
148
|
as "may have run" rather than "did not run".
|
|
140
149
|
|
|
150
|
+
A mobile runner has a touchscreen, not a mouse, so only three of the eight
|
|
151
|
+
actions have a touchscreen equivalent and go through: `click` with
|
|
152
|
+
`button: "left"` taps, `drag` swipes, and `type` types into whatever the last
|
|
153
|
+
tap focused. The rest — `double_click`, `scroll`, `move`, `keypress`,
|
|
154
|
+
`navigate` — answer `action-not-supported-on-mobile` rather than doing
|
|
155
|
+
something approximate. `navigate` is the one to watch for, since it works on a
|
|
156
|
+
browser runner without a run first but has no meaning on mobile at all.
|
|
157
|
+
|
|
141
158
|
## The recorder: what you cannot get from pixels
|
|
142
159
|
|
|
143
160
|
`qawolf runner events recorder` is the capability that has no equivalent in a
|
|
@@ -185,6 +202,52 @@ Use `inspect` before reaching for `exec`. Reading a value through a snippet
|
|
|
185
202
|
means printing it and then fishing it back out of the `console` stream, which is
|
|
186
203
|
two calls and a marker; `inspect variable` is one call and the value.
|
|
187
204
|
|
|
205
|
+
## Reading a mobile screen: `inspect session`/`contexts`/`page-source`/`elements`
|
|
206
|
+
|
|
207
|
+
`element-html`, `page-html` and `variable` are a browser's shapes. A mobile
|
|
208
|
+
runner answers four different ones instead, one subcommand per question:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
qawolf runner inspect session
|
|
212
|
+
qawolf runner inspect contexts
|
|
213
|
+
qawolf runner inspect page-source
|
|
214
|
+
qawolf runner inspect page-source --context WEBVIEW_1 # a specific context, not the current one
|
|
215
|
+
qawolf runner inspect elements --by point --x 240 --y 480
|
|
216
|
+
qawolf runner inspect elements --by text --text "Sign in" --partial
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`session` prints one summary line — ready, or why not — because that line is
|
|
220
|
+
the whole answer. `contexts`, `page-source` and `elements` instead print their
|
|
221
|
+
answer as JSON on stdout, on its own like `inspect element-html`, so you can
|
|
222
|
+
redirect or pipe it (`| jq .current`, `| jq .matches`, `| jq .pageSource`)
|
|
223
|
+
rather than getting a count with no way to see what was actually found.
|
|
224
|
+
|
|
225
|
+
`session` is also the one subcommand that never answers `screen-needs-a-run`:
|
|
226
|
+
readiness is the question it exists to answer, so it reports one of `ready`
|
|
227
|
+
(with the platform, device and session id), `unreachable`, `ambiguous` (more
|
|
228
|
+
than one session is somehow live) or `no-session`, rather than refusing until a
|
|
229
|
+
run has happened. The other three need a live session first and share the same
|
|
230
|
+
readiness contract `act` and `screenshot` use on a browser runner:
|
|
231
|
+
`screen-needs-a-run` exits `2` and means no Appium session has started on this
|
|
232
|
+
runner yet — run a flow that opens one, then inspect again; `screen-not-ready`
|
|
233
|
+
exits `4` and means the session exists but did not answer this instant, or more
|
|
234
|
+
than one is somehow live — retry once, and relaunch the runner if it persists.
|
|
235
|
+
|
|
236
|
+
`elements` takes one of two ways to search, chosen with `--by`: `point` needs
|
|
237
|
+
whole-pixel `--x`/`--y` on the device's own screen, the same coordinates a
|
|
238
|
+
screenshot is measured in; `text` needs `--text` and matches it exactly unless
|
|
239
|
+
`--partial` is passed. The two do not mix — `--by point` with `--text` set, or
|
|
240
|
+
`--by text` with `--x`/`--y` set, is refused before a runner is addressed
|
|
241
|
+
rather than silently searching by the one it picked, since the schema itself
|
|
242
|
+
just strips whichever field the chosen `by` does not define. `--context` on
|
|
243
|
+
`page-source` or `elements` reads a context other than the current one —
|
|
244
|
+
useful once `contexts` has told you which are available.
|
|
245
|
+
|
|
246
|
+
A browser runner answers `runner-is-not-mobile` to all four, exit `2`, since
|
|
247
|
+
retrying never helps: launch with `--name android` or `--name ios` instead.
|
|
248
|
+
`runner-unreachable` exits `4` and is worth retrying, same as everywhere else
|
|
249
|
+
on this surface.
|
|
250
|
+
|
|
188
251
|
## Checking a selector: `highlight-selector`
|
|
189
252
|
|
|
190
253
|
`inspect element-html` tells you what a selector matched. `highlight-selector`
|