@qawolf/cli 1.15.0 → 1.17.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 +579 -160
- package/package.json +2 -2
- package/skills/qawolf-cli/SKILL.md +2 -0
- package/skills/qawolf-cli/references/runner.md +68 -6
package/dist/cli.js
CHANGED
|
@@ -326098,11 +326098,13 @@ var interactMessages = {
|
|
|
326098
326098
|
actionFlagsWithStdin: '"-" reads the whole action from stdin, so an action flag passed alongside it would be ignored. Pipe the action in on its own, or name the action type and use flags.',
|
|
326099
326099
|
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.",
|
|
326100
326100
|
actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
|
|
326101
|
+
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.`,
|
|
326101
326102
|
actionPerformed: (type) => `Performed ${type}.`,
|
|
326102
326103
|
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.`,
|
|
326103
326104
|
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.`,
|
|
326104
326105
|
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.",
|
|
326105
326106
|
runnerHasNoScreenToEvaluate: "The runner could not evaluate the snippet. This covers a runner that is still starting or busy, and also one with no live page to evaluate against: a freshly launched runner has no page until a run opens one, so run a flow on it with qawolf runner run first. If it is not a runner that runs a browser at all, this will never clear. It is not proof the snippet did not run, so do not resubmit one that mutates the page without reading qawolf runner events console first.",
|
|
326107
|
+
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.",
|
|
326106
326108
|
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.`,
|
|
326107
326109
|
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.`,
|
|
326108
326110
|
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.",
|
|
@@ -326121,7 +326123,18 @@ var interactMessages = {
|
|
|
326121
326123
|
packageInstalled: (name, version) => `Installed ${name}@${version} into the runner's live run.`,
|
|
326122
326124
|
packageJsonUnreadable: (reason) => `package.json could not be read because ${reason}. Fix it and try again.`,
|
|
326123
326125
|
installFailed: (name, version, errorMessage2) => `npm could not install ${name}@${version}${errorMessage2 === undefined ? "" : `. ${errorMessage2}`}. Check the name and the version; waiting will not change this.`,
|
|
326124
|
-
importAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli
|
|
326126
|
+
importAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
326127
|
+
highlightCleared: "Cleared the runner's highlight. Its next screenshot shows the page undrawn on.",
|
|
326128
|
+
highlightMatched: (matchCount, targetPage) => `Highlighted ${pluralize(matchCount, "element", "elements")}${targetPage === undefined ? "" : ` on ${targetPage}`}. The highlight stays until it is replaced or cleared, so take a screenshot to see it.`,
|
|
326129
|
+
highlightMatchedNothing: (selector) => `The page read "${selector}" as a selector and nothing matched it, so nothing is highlighted. The selector is well formed, so this is the page rather than the syntax: check what is actually on it with qawolf runner inspect page-html.`,
|
|
326130
|
+
highlightSelectorInvalid: (selector) => `The page could not read "${selector}" as a selector, so nothing was highlighted. This is the syntax rather than the page, and retrying will not change it.`,
|
|
326131
|
+
highlightNoAnswer: "The page did not answer, so nothing is highlighted. A highlight runs inside the page, so a page that is gone or part-way through navigating does not answer at all rather than answering slowly. Take a screenshot to see where the runner is, then try again.",
|
|
326132
|
+
runnerCannotHighlightSelectors: "This runner has no browser to draw on, so there is nothing to highlight. Retrying will never help: launch a playwright runner instead.",
|
|
326133
|
+
highlightAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
326134
|
+
snapshotPromoted: (screenshotPath, baselinePath) => `Promoted ${screenshotPath} to the baseline at ${baselinePath}.`,
|
|
326135
|
+
snapshotNotFound: (screenshotPath) => `The run wrote no screenshot at "${screenshotPath}", so nothing was changed. The two paths have to be the ones an image diff reported: read them from the image-diff-artifact entry on qawolf runner events run-events.`,
|
|
326136
|
+
runnerCannotPromoteSnapshots: "This runner stores no screenshots, so it holds no baseline to replace. Retrying will never help.",
|
|
326137
|
+
promoteSnapshotAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`
|
|
326125
326138
|
};
|
|
326126
326139
|
|
|
326127
326140
|
// src/core/messages/interactiveRunner/lifecycle.ts
|
|
@@ -326136,7 +326149,9 @@ var lifecycleMessages = {
|
|
|
326136
326149
|
launched: (id) => `Launched runner ${id}.`,
|
|
326137
326150
|
launchedForCommand: (id) => `No runner was given, so launched ${id} for this command. Its browser is fresh: nothing has been run on it and nothing is signed in. It bills until it is terminated or idles out, so terminate it with qawolf runner terminate --runner ${id} when you are done.`,
|
|
326138
326151
|
noRunnerIdForImport: `${noRunnerId} An install also needs a live run to go into, which a runner gets from qawolf runner run.`,
|
|
326152
|
+
noRunnerIdForHighlight: `${noRunnerId} Highlighting also needs a live page to draw on, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
|
|
326139
326153
|
noRunnerIdForInspect: `${noRunnerId} Inspecting also needs a page, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
|
|
326154
|
+
noRunnerIdForPromoteSnapshot: `${noRunnerId} A baseline is replaced on the runner that produced the screenshot, so it has to be the runner the diff came from.`,
|
|
326140
326155
|
noRunnerIdForScreenshot: `${noRunnerId} A screenshot also needs a screen, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
|
|
326141
326156
|
noRunnerId,
|
|
326142
326157
|
wasNotRunning: (id) => `Runner ${id} was not running.`,
|
|
@@ -326150,6 +326165,8 @@ var lifecycleMessages = {
|
|
|
326150
326165
|
var runMessages = {
|
|
326151
326166
|
envFileUnparseable: (reason) => `The --env-file could not be read. ${reason}`,
|
|
326152
326167
|
envFileUnreadable: (path) => `No env file at ${path}. Pass a path that exists, or drop --env-file to send no environment.`,
|
|
326168
|
+
envIdBlank: "--env-id was given nothing. Pass the id or alias of a QA Wolf environment, or drop the flag to send no environment.",
|
|
326169
|
+
envIdWithEnvFile: "--env-id and --env-file both give the run its environment, so only one may be passed. Use --env-id for a QA Wolf environment, which QA Wolf reads itself, or --env-file for a dotenv file on this machine.",
|
|
326153
326170
|
fileNotCollected: (path) => `"${path}" is not one of the files that travel to a runner. It must be inside the current directory and end in .ts, .tsx, .js, .mjs, .cjs or .json.`,
|
|
326154
326171
|
fileUnreadable: (path, reason) => `${path} is one of the files that travel to a runner, but it could not be read: ${reason}. Move it out of the current directory, or make it readable.`,
|
|
326155
326172
|
filesUnreadable: (reason) => `One of the files that travel to a runner could not be read: ${reason}.`,
|
|
@@ -342183,7 +342200,7 @@ var makeCreateRunContract = (ids) => {
|
|
|
342183
342200
|
aiTaskId: ids.aiTask.describe("The AI task whose conversation should receive run status updates.").optional(),
|
|
342184
342201
|
environmentId: ids.environmentRef,
|
|
342185
342202
|
environmentVariables: exports_external.record(exports_external.string(), exports_external.string()).optional(),
|
|
342186
|
-
ignoreRules: exports_external.boolean().default(false),
|
|
342203
|
+
ignoreRules: exports_external.boolean().describe("Ignore this team's sequencing rules for this run. By default they are honoured: the flows your selection depends on are added to the run, and flows run in the order the rules require. Set this to run exactly what you selected, in any order.").default(false),
|
|
342187
342204
|
pullRequestNumber: exports_external.number().int().positive().describe("The number of the pull request to associate with the run.").optional(),
|
|
342188
342205
|
repository: exports_external.string().min(1).describe('The owner/name of the repository, e.g. "acme/web".').optional()
|
|
342189
342206
|
}).and(makeFlowSelectionSchema(ids));
|
|
@@ -342272,8 +342289,8 @@ function refusalForRunEnvironmentVariableName(name) {
|
|
|
342272
342289
|
return `${reservedRunEnvironmentVariableName} is reserved by QA Wolf and set for you.`;
|
|
342273
342290
|
return;
|
|
342274
342291
|
}
|
|
342275
|
-
var maxRunEnvironmentVariables =
|
|
342276
|
-
var maxRunEnvironmentValueLength =
|
|
342292
|
+
var maxRunEnvironmentVariables = 200;
|
|
342293
|
+
var maxRunEnvironmentValueLength = 16 * 1024;
|
|
342277
342294
|
var runEnvironmentSchema = exports_external.record(exports_external.string(), exports_external.string().max(maxRunEnvironmentValueLength)).superRefine((environment, refinementContext) => {
|
|
342278
342295
|
for (const name of Object.keys(environment)) {
|
|
342279
342296
|
const refusal = refusalForRunEnvironmentVariableName(name);
|
|
@@ -342366,18 +342383,167 @@ var makeInspectOnRunnerContract = () => {
|
|
|
342366
342383
|
}),
|
|
342367
342384
|
exports_external.object({
|
|
342368
342385
|
errorMessage: exports_external.string().optional().describe("What the runner said, when it said anything."),
|
|
342369
|
-
failureReason: exports_external.enum([
|
|
342386
|
+
failureReason: exports_external.enum([
|
|
342387
|
+
"nothing-to-inspect",
|
|
342388
|
+
"runner-is-not-a-browser",
|
|
342389
|
+
"runner-unreachable"
|
|
342390
|
+
]),
|
|
342370
342391
|
outcome: exports_external.literal("failure")
|
|
342371
342392
|
})
|
|
342372
342393
|
]);
|
|
342373
342394
|
return {
|
|
342374
|
-
description: "Inspect one thing on an interactive runner: an element's HTML, the page's HTML simplified for a model, or a top-level variable's value as JSON. `nothing-to-inspect` means the runner had nothing to answer with: no live page, no element matching the selector, or no variable under that name. A runner cannot tell those apart, so it reports the one reason and puts whatever it did say in `errorMessage`. " + retryableRunnerUnreachableDescription,
|
|
342395
|
+
description: "Inspect one thing on an interactive runner: an element's HTML, the page's HTML simplified for a model, or a top-level variable's value as JSON. `nothing-to-inspect` means the runner had nothing to answer with: no live page, no element matching the selector, or no variable under that name. A runner cannot tell those apart, so it reports the one reason and puts whatever it did say in `errorMessage`. `runner-is-not-a-browser` on a mobile runner — call `runner.inspectMobile` for its equivalent surface instead. " + retryableRunnerUnreachableDescription,
|
|
342375
342396
|
input,
|
|
342376
342397
|
kind: "read",
|
|
342377
342398
|
name: "runner.inspect",
|
|
342378
342399
|
output
|
|
342379
342400
|
};
|
|
342380
342401
|
};
|
|
342402
|
+
// node_modules/@qawolf/api-contracts/dist/v1/runner/inspectMobileShapes.js
|
|
342403
|
+
var boundsSchema = exports_external.object({
|
|
342404
|
+
bottom: exports_external.number(),
|
|
342405
|
+
left: exports_external.number(),
|
|
342406
|
+
right: exports_external.number(),
|
|
342407
|
+
top: exports_external.number()
|
|
342408
|
+
});
|
|
342409
|
+
var pointSchema = exports_external.object({ x: exports_external.number(), y: exports_external.number() });
|
|
342410
|
+
var selectorSchema = exports_external.object({
|
|
342411
|
+
strategy: exports_external.union([
|
|
342412
|
+
exports_external.object({
|
|
342413
|
+
name: exports_external.literal("xpath"),
|
|
342414
|
+
type: exports_external.enum(["unique", "full-path", "sibling-of", "parent-of"])
|
|
342415
|
+
}),
|
|
342416
|
+
exports_external.object({ name: exports_external.literal("ios-predicate") }),
|
|
342417
|
+
exports_external.object({
|
|
342418
|
+
hostSelector: exports_external.string(),
|
|
342419
|
+
innerSelector: exports_external.string(),
|
|
342420
|
+
name: exports_external.literal("shadow")
|
|
342421
|
+
})
|
|
342422
|
+
]),
|
|
342423
|
+
value: exports_external.string()
|
|
342424
|
+
});
|
|
342425
|
+
var elementSchema = exports_external.object({
|
|
342426
|
+
attributes: exports_external.record(exports_external.string(), exports_external.string()),
|
|
342427
|
+
bounds: boundsSchema.optional().describe("The element's on-screen rectangle, absent off-screen."),
|
|
342428
|
+
center: pointSchema.optional().describe("Derived from `bounds`; absent wherever `bounds` is."),
|
|
342429
|
+
selectors: selectorSchema.array().describe("Ways to address this element again, most reliable first."),
|
|
342430
|
+
tag: exports_external.string()
|
|
342431
|
+
});
|
|
342432
|
+
var basePageSourceNodeSchema = exports_external.object({
|
|
342433
|
+
attributes: exports_external.object({
|
|
342434
|
+
bounds: boundsSchema.optional(),
|
|
342435
|
+
rest: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
342436
|
+
}).optional(),
|
|
342437
|
+
selectors: selectorSchema.array(),
|
|
342438
|
+
tag: exports_external.string()
|
|
342439
|
+
});
|
|
342440
|
+
var pageSourceNodeSchema = basePageSourceNodeSchema.extend({
|
|
342441
|
+
children: exports_external.lazy(() => exports_external.union([pageSourceNodeSchema, exports_external.string()]).array()).optional().describe("A string child is a text node; there is no element to select.")
|
|
342442
|
+
});
|
|
342443
|
+
var orientationSchema = exports_external.enum(["PORTRAIT", "LANDSCAPE"]);
|
|
342444
|
+
|
|
342445
|
+
// node_modules/@qawolf/api-contracts/dist/v1/runner/outcome.js
|
|
342446
|
+
function makeRunnerFailureSchema(reasons) {
|
|
342447
|
+
return exports_external.object({
|
|
342448
|
+
failureReason: exports_external.enum(reasons),
|
|
342449
|
+
outcome: exports_external.literal("failure")
|
|
342450
|
+
});
|
|
342451
|
+
}
|
|
342452
|
+
|
|
342453
|
+
// node_modules/@qawolf/api-contracts/dist/v1/runner/inspectMobile.js
|
|
342454
|
+
var maxElementTextLength = 500;
|
|
342455
|
+
var inspectMobileRequestSchema = exports_external.discriminatedUnion("what", [
|
|
342456
|
+
exports_external.object({
|
|
342457
|
+
what: exports_external.literal("session").describe("The status of the Appium session driving the device: ready and what it is, or why not.")
|
|
342458
|
+
}),
|
|
342459
|
+
exports_external.object({
|
|
342460
|
+
what: exports_external.literal("contexts").describe("The WebView contexts available on the device, and which one is current.")
|
|
342461
|
+
}),
|
|
342462
|
+
exports_external.object({
|
|
342463
|
+
context: exports_external.string().min(1).optional().describe("Read this context instead of whichever is current."),
|
|
342464
|
+
what: exports_external.literal("page").describe("The page source of the device's current context, as a tree.")
|
|
342465
|
+
}),
|
|
342466
|
+
exports_external.discriminatedUnion("by", [
|
|
342467
|
+
exports_external.object({
|
|
342468
|
+
by: exports_external.literal("point"),
|
|
342469
|
+
context: exports_external.string().min(1).optional(),
|
|
342470
|
+
what: exports_external.literal("elements"),
|
|
342471
|
+
x: exports_external.int().min(0).describe("Whole pixels on the device's own screen."),
|
|
342472
|
+
y: exports_external.int().min(0).describe("Whole pixels on the device's own screen.")
|
|
342473
|
+
}),
|
|
342474
|
+
exports_external.object({
|
|
342475
|
+
by: exports_external.literal("text"),
|
|
342476
|
+
context: exports_external.string().min(1).optional(),
|
|
342477
|
+
partial: exports_external.boolean().optional().describe("Match text containing this, rather than exactly this."),
|
|
342478
|
+
text: exports_external.string().min(1).max(maxElementTextLength),
|
|
342479
|
+
what: exports_external.literal("elements")
|
|
342480
|
+
})
|
|
342481
|
+
]).describe("Elements at a screen point, or elements carrying some text — never both at once.")
|
|
342482
|
+
]);
|
|
342483
|
+
var inspectMobileAnswerSchema = exports_external.discriminatedUnion("what", [
|
|
342484
|
+
exports_external.object({
|
|
342485
|
+
outcome: exports_external.literal("success"),
|
|
342486
|
+
session: exports_external.discriminatedUnion("type", [
|
|
342487
|
+
exports_external.object({
|
|
342488
|
+
deviceName: exports_external.string().optional(),
|
|
342489
|
+
platformName: exports_external.string(),
|
|
342490
|
+
sessionId: exports_external.string(),
|
|
342491
|
+
type: exports_external.literal("ready")
|
|
342492
|
+
}),
|
|
342493
|
+
exports_external.object({
|
|
342494
|
+
error: exports_external.string(),
|
|
342495
|
+
type: exports_external.literal("unreachable")
|
|
342496
|
+
}),
|
|
342497
|
+
exports_external.object({
|
|
342498
|
+
sessionCount: exports_external.number(),
|
|
342499
|
+
type: exports_external.literal("ambiguous")
|
|
342500
|
+
}),
|
|
342501
|
+
exports_external.object({ type: exports_external.literal("no-session") })
|
|
342502
|
+
]),
|
|
342503
|
+
what: exports_external.literal("session")
|
|
342504
|
+
}),
|
|
342505
|
+
exports_external.object({
|
|
342506
|
+
contexts: exports_external.string().array(),
|
|
342507
|
+
current: exports_external.string(),
|
|
342508
|
+
outcome: exports_external.literal("success"),
|
|
342509
|
+
what: exports_external.literal("contexts")
|
|
342510
|
+
}),
|
|
342511
|
+
exports_external.object({
|
|
342512
|
+
context: exports_external.string(),
|
|
342513
|
+
orientation: orientationSchema,
|
|
342514
|
+
outcome: exports_external.literal("success"),
|
|
342515
|
+
pageSource: pageSourceNodeSchema,
|
|
342516
|
+
what: exports_external.literal("page")
|
|
342517
|
+
}),
|
|
342518
|
+
exports_external.object({
|
|
342519
|
+
matches: elementSchema.array(),
|
|
342520
|
+
outcome: exports_external.literal("success"),
|
|
342521
|
+
what: exports_external.literal("elements")
|
|
342522
|
+
})
|
|
342523
|
+
]);
|
|
342524
|
+
var makeInspectMobileOnRunnerContract = () => {
|
|
342525
|
+
const input = exports_external.object({
|
|
342526
|
+
id: runnerIdSchema.describe("Id of the runner to inspect."),
|
|
342527
|
+
request: inspectMobileRequestSchema
|
|
342528
|
+
});
|
|
342529
|
+
const output = exports_external.discriminatedUnion("outcome", [
|
|
342530
|
+
inspectMobileAnswerSchema,
|
|
342531
|
+
makeRunnerFailureSchema([
|
|
342532
|
+
"runner-is-not-mobile",
|
|
342533
|
+
"screen-needs-a-run",
|
|
342534
|
+
"screen-not-ready",
|
|
342535
|
+
runnerUnreachableFailureReason
|
|
342536
|
+
])
|
|
342537
|
+
]);
|
|
342538
|
+
return {
|
|
342539
|
+
description: `Inspect one thing on a mobile interactive runner: the Appium session's status, the WebView contexts available, the current context's page source, or the elements at a point or carrying some text. Mobile only — a browser runner answers \`runner-is-not-mobile\`; call \`runner.inspect\` for a browser's equivalent surface instead. \`what: "session"\` always answers with the session's own status rather than \`screen-needs-a-run\`, since that is the question it exists to answer; the other three request kinds need a live session first and answer the same \`screen-needs-a-run\` or \`screen-not-ready\` outcomes \`runner.performAction\` and \`runner.takeScreenshot\` use, instead of reading anything when there is none. \`screen-needs-a-run\` means no Appium session has started on this runner yet — call \`runner.runFlow\` with a flow that opens one, then inspect again. \`screen-not-ready\` means the runner's Appium session exists but did not answer this instant, or more than one is somehow live — retry once; if it persists, relaunch the runner. \`runner-is-not-mobile\` means there is nothing here to inspect, and retrying will never help — launch an \`android\` or \`ios\` runner instead. ${retryableRunnerUnreachableDescription}`,
|
|
342540
|
+
input,
|
|
342541
|
+
kind: "read",
|
|
342542
|
+
name: "runner.inspectMobile",
|
|
342543
|
+
output
|
|
342544
|
+
};
|
|
342545
|
+
};
|
|
342546
|
+
|
|
342381
342547
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/journal.js
|
|
342382
342548
|
var journalEntrySchema = exports_external.object({
|
|
342383
342549
|
payload: exports_external.unknown(),
|
|
@@ -342434,14 +342600,6 @@ var runFilesSchema = exports_external.record(exports_external.string(), exports_
|
|
|
342434
342600
|
}).refine((files) => runFilesByteLength(files) <= maxRunFilesByteLength, {
|
|
342435
342601
|
error: `The files in one request may carry at most ${maxRunFilesByteLength} bytes in total.`
|
|
342436
342602
|
});
|
|
342437
|
-
// node_modules/@qawolf/api-contracts/dist/v1/runner/outcome.js
|
|
342438
|
-
function makeRunnerFailureSchema(reasons) {
|
|
342439
|
-
return exports_external.object({
|
|
342440
|
-
failureReason: exports_external.enum(reasons),
|
|
342441
|
-
outcome: exports_external.literal("failure")
|
|
342442
|
-
});
|
|
342443
|
-
}
|
|
342444
|
-
|
|
342445
342603
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/screen.js
|
|
342446
342604
|
var screenFailureReasons = [
|
|
342447
342605
|
"runner-has-no-screen",
|
|
@@ -342454,6 +342612,7 @@ var runnerHasNoScreenDescription = "`runner-has-no-screen` if this runner is not
|
|
|
342454
342612
|
|
|
342455
342613
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/performAction.js
|
|
342456
342614
|
var unreachableDescription = "`runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may have stopped answering mid-action. This does not mean the action was not performed — take a screenshot before repeating it.";
|
|
342615
|
+
var notSupportedOnMobileDescription = "`action-not-supported-on-mobile` if the runner is a mobile device and this action has no touchscreen equivalent: `double_click`, `scroll`, `move`, `keypress` and `navigate`, and a `click` whose `button` is not `left`, are all pointer-device concepts a touchscreen has nothing to offer for. `click` taps, `drag` swipes between its path's first and last point, and `type` types into whatever the last tap focused.";
|
|
342457
342616
|
var maxActionErrorMessageLength = 1000;
|
|
342458
342617
|
var makePerformActionOnRunnerContract = () => {
|
|
342459
342618
|
const input = exports_external.object({
|
|
@@ -342468,6 +342627,7 @@ var makePerformActionOnRunnerContract = () => {
|
|
|
342468
342627
|
}),
|
|
342469
342628
|
makeRunnerFailureSchema([
|
|
342470
342629
|
...screenFailureReasons,
|
|
342630
|
+
"action-not-supported-on-mobile",
|
|
342471
342631
|
runnerUnreachableFailureReason
|
|
342472
342632
|
])
|
|
342473
342633
|
]);
|
|
@@ -342476,7 +342636,7 @@ var makePerformActionOnRunnerContract = () => {
|
|
|
342476
342636
|
failure
|
|
342477
342637
|
]);
|
|
342478
342638
|
return {
|
|
342479
|
-
description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${unreachableDescription}`,
|
|
342639
|
+
description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. On a mobile runner, the same \`screen-needs-a-run\` and \`screen-not-ready\` outcomes mean no Appium session has started yet, or it did not answer this instant; \`runner.runFlow\` is what starts one, same as a browser. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${notSupportedOnMobileDescription} ${unreachableDescription}`,
|
|
342480
342640
|
input,
|
|
342481
342641
|
kind: "write",
|
|
342482
342642
|
name: "runner.performAction",
|
|
@@ -343082,6 +343242,43 @@ var makeEvaluateSnippetOnRunnerContract = () => {
|
|
|
343082
343242
|
};
|
|
343083
343243
|
};
|
|
343084
343244
|
|
|
343245
|
+
// node_modules/@qawolf/api-contracts/dist/v1/runner/highlightSelector.js
|
|
343246
|
+
var maxHighlightSelectorLength = 2000;
|
|
343247
|
+
var makeHighlightSelectorOnRunnerContract = () => {
|
|
343248
|
+
const input = exports_external.object({
|
|
343249
|
+
id: runnerIdSchema.describe("Id of the runner to highlight on."),
|
|
343250
|
+
selector: exports_external.string().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted.")
|
|
343251
|
+
});
|
|
343252
|
+
const output = exports_external.discriminatedUnion("outcome", [
|
|
343253
|
+
exports_external.object({
|
|
343254
|
+
matchCount: exports_external.number().int().gte(0).describe("How many elements the selector matched."),
|
|
343255
|
+
outcome: exports_external.literal("success"),
|
|
343256
|
+
selector: exports_external.string().describe("The selector the page matched on."),
|
|
343257
|
+
status: exports_external.literal(["valid", "invalid", "empty"]).describe("`valid` when the selector parsed and matched, `empty` when it parsed and matched nothing, `invalid` when the page could not read it as a selector."),
|
|
343258
|
+
targetPage: exports_external.string().optional().describe("Present when the match was on a page other than the default one.")
|
|
343259
|
+
}),
|
|
343260
|
+
exports_external.object({
|
|
343261
|
+
outcome: exports_external.literal("cleared")
|
|
343262
|
+
}),
|
|
343263
|
+
exports_external.object({
|
|
343264
|
+
errorMessage: exports_external.string().optional(),
|
|
343265
|
+
failureReason: exports_external.enum([
|
|
343266
|
+
"no-answer",
|
|
343267
|
+
"runner-cannot-highlight-selectors",
|
|
343268
|
+
"runner-unreachable"
|
|
343269
|
+
]),
|
|
343270
|
+
outcome: exports_external.literal("failure")
|
|
343271
|
+
})
|
|
343272
|
+
]);
|
|
343273
|
+
return {
|
|
343274
|
+
description: "Highlight the elements a selector matches on an interactive runner's live page, and answer how many it matched. The highlight stays until it is replaced or cleared, so it is visible in the next `runner.takeScreenshot` — which is the point, since a caller cannot see the runner's screen otherwise. Send an empty selector to clear, which answers `cleared`. `status` tells a selector that matched nothing (`empty`) from one the page could not parse (`invalid`), so a caller can tell a bad locator from a locator pointing at nothing. `no-answer` if the page did not answer in time: the highlight runs inside the page, so a page that is gone or mid-navigation does not answer slowly, it does not answer at all. `runner-cannot-highlight-selectors` on a runner with no browser to draw on. `runner-unreachable` if the runner could not be reached: it may still be starting, or it may have terminated after inactivity.",
|
|
343275
|
+
input,
|
|
343276
|
+
kind: "write",
|
|
343277
|
+
name: "runner.highlightSelector",
|
|
343278
|
+
output
|
|
343279
|
+
};
|
|
343280
|
+
};
|
|
343281
|
+
|
|
343085
343282
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/importPackage.js
|
|
343086
343283
|
var maxPackageNameLength = 214;
|
|
343087
343284
|
var maxPackageVersionLength = 256;
|
|
@@ -343145,6 +343342,34 @@ var makeTerminateRunnerContract = () => {
|
|
|
343145
343342
|
};
|
|
343146
343343
|
};
|
|
343147
343344
|
|
|
343345
|
+
// node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
|
|
343346
|
+
var maxSnapshotPathLength = 1024;
|
|
343347
|
+
var makePromoteSnapshotOnRunnerContract = () => {
|
|
343348
|
+
const input = exports_external.object({
|
|
343349
|
+
baselinePath: exports_external.string().min(1).max(maxSnapshotPathLength).describe("The baseline to replace, as named in the image diff."),
|
|
343350
|
+
id: runnerIdSchema.describe("Id of the runner holding the screenshot."),
|
|
343351
|
+
screenshotPath: exports_external.string().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff.")
|
|
343352
|
+
});
|
|
343353
|
+
const output = exports_external.discriminatedUnion("outcome", [
|
|
343354
|
+
exports_external.object({ outcome: exports_external.literal("success") }),
|
|
343355
|
+
exports_external.object({
|
|
343356
|
+
failureReason: exports_external.enum([
|
|
343357
|
+
"runner-cannot-promote-snapshots",
|
|
343358
|
+
"runner-unreachable",
|
|
343359
|
+
"snapshot-not-found"
|
|
343360
|
+
]),
|
|
343361
|
+
outcome: exports_external.literal("failure")
|
|
343362
|
+
})
|
|
343363
|
+
]);
|
|
343364
|
+
return {
|
|
343365
|
+
description: "Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it. The two paths are the ones the diff reported, which reach a caller as an `image-diff-artifact` entry on the runner's `run-events` journal stream. `snapshot-not-found` if the run wrote no screenshot at that path, which usually means the paths were not taken from a diff this run produced; nothing is changed, so correcting the path and repeating is safe. `runner-cannot-promote-snapshots` on a runner that stores no screenshots. `runner-unreachable` if the runner could not be reached: it may still be starting, or it may have terminated after inactivity. A promotion that lands is not undone by promoting again, so a retry after an unreachable answer is safe.",
|
|
343366
|
+
input,
|
|
343367
|
+
kind: "write",
|
|
343368
|
+
name: "runner.promoteSnapshot",
|
|
343369
|
+
output
|
|
343370
|
+
};
|
|
343371
|
+
};
|
|
343372
|
+
|
|
343148
343373
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/readJournal.js
|
|
343149
343374
|
var makeReadRunnerJournalContract = () => {
|
|
343150
343375
|
const input = readJournalRequestSchema.extend({
|
|
@@ -343176,14 +343401,18 @@ var unchangedFilesSchema = exports_external.record(exports_external.string(), ex
|
|
|
343176
343401
|
});
|
|
343177
343402
|
}
|
|
343178
343403
|
});
|
|
343179
|
-
var makeRunFlowOnRunnerContract = () => {
|
|
343404
|
+
var makeRunFlowOnRunnerContract = (ids) => {
|
|
343180
343405
|
const input = exports_external.object({
|
|
343181
343406
|
entryPointPath: runFilePathSchema.describe("Path of the flow file to run, as it appears in `files`."),
|
|
343182
343407
|
env: runEnvironmentSchema.optional().describe("Environment variables to make available to the run."),
|
|
343408
|
+
environmentId: ids.environmentRef.optional().describe("A QA Wolf environment whose variables the run is given, by id or alias. QA Wolf reads and decrypts them itself, so they never travel in the request and the count and length limits on `env` do not apply to them. Send this or `env`, not both."),
|
|
343183
343409
|
files: runFilesSchema.describe("Every file the run needs keyed by its path — the flow file, everything it imports, package.json and tsconfig.json. A runner holds no copy of your project, so what runs is exactly what is sent here."),
|
|
343184
343410
|
id: runnerIdSchema.describe("Id of the runner to run the flow on."),
|
|
343185
343411
|
selection: runSelectionSchema.optional().describe("Run only these lines, inside the page the runner is already on, instead of running the whole flow from a fresh browser. Omit to run the whole entry point."),
|
|
343186
343412
|
unchangedFiles: unchangedFilesSchema.optional().describe("Files this runner already holds from an earlier run, keyed by path with a hex SHA-256 of the content it should hold. Send this to make `files` only what changed. The runner refuses with `needs-full-sync` if it holds none of a referenced path, so it never runs something other than what was asked for. Omit it to send every file.")
|
|
343413
|
+
}).refine((request) => request.env === undefined || request.environmentId === undefined, {
|
|
343414
|
+
error: "Send env or environmentId, not both. An environment's variables are taken as they are stored, so there is no order in which the two would combine.",
|
|
343415
|
+
path: ["environmentId"]
|
|
343187
343416
|
}).refine((request) => Object.hasOwn(request.files, request.entryPointPath), {
|
|
343188
343417
|
error: "The entry point must be one of the files.",
|
|
343189
343418
|
path: ["files"]
|
|
@@ -343268,7 +343497,7 @@ var makeTakeScreenshotOnRunnerContract = () => {
|
|
|
343268
343497
|
])
|
|
343269
343498
|
]);
|
|
343270
343499
|
return {
|
|
343271
|
-
description: `Take one screenshot of an interactive runner's screen.
|
|
343500
|
+
description: `Take one screenshot of an interactive runner's screen. On a runner with a browser the image is the whole virtual desktop, browser window and all. On a mobile runner it is the device's own screen, re-encoded to JPEG on the pod so this contract reads one image format regardless of runner family. ${screenNeedsARunDescription} ${screenNotReadyDescription} ${runnerHasNoScreenDescription} ${retryableRunnerUnreachableDescription}`,
|
|
343272
343501
|
input,
|
|
343273
343502
|
kind: "read",
|
|
343274
343503
|
name: "runner.takeScreenshot",
|
|
@@ -343334,12 +343563,15 @@ var makeContractsV1 = (ids) => {
|
|
|
343334
343563
|
},
|
|
343335
343564
|
runner: {
|
|
343336
343565
|
evaluateSnippet: makeEvaluateSnippetOnRunnerContract(),
|
|
343566
|
+
highlightSelector: makeHighlightSelectorOnRunnerContract(),
|
|
343337
343567
|
importPackage: makeImportPackageOnRunnerContract(),
|
|
343338
343568
|
inspect: makeInspectOnRunnerContract(),
|
|
343569
|
+
inspectMobile: makeInspectMobileOnRunnerContract(),
|
|
343339
343570
|
launch: makeLaunchRunnerContract(),
|
|
343340
343571
|
performAction: makePerformActionOnRunnerContract(),
|
|
343572
|
+
promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
|
|
343341
343573
|
readJournal: makeReadRunnerJournalContract(),
|
|
343342
|
-
runFlow: makeRunFlowOnRunnerContract(),
|
|
343574
|
+
runFlow: makeRunFlowOnRunnerContract(resolvedIds),
|
|
343343
343575
|
stopRun: makeStopRunOnRunnerContract(),
|
|
343344
343576
|
takeScreenshot: makeTakeScreenshotOnRunnerContract(),
|
|
343345
343577
|
terminate: makeTerminateRunnerContract()
|
|
@@ -345866,7 +346098,7 @@ function startUpdateCheck(deps) {
|
|
|
345866
346098
|
// package.json
|
|
345867
346099
|
var package_default = {
|
|
345868
346100
|
name: "@qawolf/cli",
|
|
345869
|
-
version: "1.
|
|
346101
|
+
version: "1.17.0",
|
|
345870
346102
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
345871
346103
|
keywords: [
|
|
345872
346104
|
"automation",
|
|
@@ -345926,7 +346158,7 @@ var package_default = {
|
|
|
345926
346158
|
"@clack/prompts": "1.5.1",
|
|
345927
346159
|
"@napi-rs/keyring": "1.3.0",
|
|
345928
346160
|
"@oxc-node/core": "0.1.0",
|
|
345929
|
-
"@qawolf/api-contracts": "0.
|
|
346161
|
+
"@qawolf/api-contracts": "0.34.0",
|
|
345930
346162
|
"@qawolf/emails": "1.1.1",
|
|
345931
346163
|
"@qawolf/flow-targets": "1.0.0",
|
|
345932
346164
|
"@qawolf/flows": "0.1.4",
|
|
@@ -356360,10 +356592,13 @@ async function handlePublicApiCommand(ctx, spec, options) {
|
|
|
356360
356592
|
var skippedContractNames = new Set([
|
|
356361
356593
|
"flow.list",
|
|
356362
356594
|
"runner.evaluateSnippet",
|
|
356595
|
+
"runner.highlightSelector",
|
|
356363
356596
|
"runner.importPackage",
|
|
356364
356597
|
"runner.inspect",
|
|
356598
|
+
"runner.inspectMobile",
|
|
356365
356599
|
"runner.launch",
|
|
356366
356600
|
"runner.performAction",
|
|
356601
|
+
"runner.promoteSnapshot",
|
|
356367
356602
|
"runner.readJournal",
|
|
356368
356603
|
"runner.runFlow",
|
|
356369
356604
|
"runner.stopRun",
|
|
@@ -356422,47 +356657,8 @@ function registerPublicApiCommands(program2, signals, options = {}) {
|
|
|
356422
356657
|
}
|
|
356423
356658
|
}
|
|
356424
356659
|
|
|
356425
|
-
// src/domains/interactiveRunner/importPackage.ts
|
|
356426
|
-
import { join as join44 } from "node:path";
|
|
356427
|
-
|
|
356428
|
-
// src/core/interactiveRunner/npmDependencies.ts
|
|
356429
|
-
var sectionsInPrecedenceOrder = ["dependencies", "devDependencies"];
|
|
356430
|
-
var isInstallable = (version2) => !version2.startsWith("workspace:");
|
|
356431
|
-
function isRecord5(value) {
|
|
356432
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
356433
|
-
}
|
|
356434
|
-
function readNpmDependencies(packageJsonContent) {
|
|
356435
|
-
let parsed;
|
|
356436
|
-
try {
|
|
356437
|
-
parsed = JSON.parse(packageJsonContent);
|
|
356438
|
-
} catch {
|
|
356439
|
-
return { ok: false, reason: "it is not valid JSON" };
|
|
356440
|
-
}
|
|
356441
|
-
if (!isRecord5(parsed))
|
|
356442
|
-
return { ok: false, reason: "it is not an object" };
|
|
356443
|
-
const dependencies = {};
|
|
356444
|
-
for (const section of sectionsInPrecedenceOrder) {
|
|
356445
|
-
const declared = parsed[section];
|
|
356446
|
-
if (declared === undefined)
|
|
356447
|
-
continue;
|
|
356448
|
-
if (!isRecord5(declared)) {
|
|
356449
|
-
return { ok: false, reason: `${section} is not an object` };
|
|
356450
|
-
}
|
|
356451
|
-
for (const [name, version2] of Object.entries(declared)) {
|
|
356452
|
-
if (typeof version2 !== "string") {
|
|
356453
|
-
return { ok: false, reason: `${section}.${name} is not a string` };
|
|
356454
|
-
}
|
|
356455
|
-
if (Object.hasOwn(dependencies, name))
|
|
356456
|
-
continue;
|
|
356457
|
-
if (isInstallable(version2))
|
|
356458
|
-
dependencies[name] = version2;
|
|
356459
|
-
}
|
|
356460
|
-
}
|
|
356461
|
-
return { dependencies, ok: true };
|
|
356462
|
-
}
|
|
356463
|
-
|
|
356464
356660
|
// src/domains/interactiveRunner/runnerCallOptions.ts
|
|
356465
|
-
var runnerCallOptions = { timeoutMs:
|
|
356661
|
+
var runnerCallOptions = { timeoutMs: 90000 };
|
|
356466
356662
|
|
|
356467
356663
|
// src/domains/interactiveRunner/runnerIds.ts
|
|
356468
356664
|
function parseRunnerId(id) {
|
|
@@ -356575,71 +356771,73 @@ function announceRunner(ctx, resolved) {
|
|
|
356575
356771
|
}
|
|
356576
356772
|
}
|
|
356577
356773
|
|
|
356578
|
-
// src/domains/interactiveRunner/
|
|
356579
|
-
|
|
356580
|
-
async function handleRunnerImportPackage(ctx, options, deps) {
|
|
356581
|
-
const content = await deps.readFile(join44(deps.cwd, runPackageJsonPath)).catch(() => {
|
|
356582
|
-
return;
|
|
356583
|
-
});
|
|
356584
|
-
if (content === undefined) {
|
|
356585
|
-
return {
|
|
356586
|
-
error: interactiveRunnerMessages.missingPackageJsonForImport,
|
|
356587
|
-
exitCode: exitCodes.config
|
|
356588
|
-
};
|
|
356589
|
-
}
|
|
356590
|
-
const dependencies = readNpmDependencies(content);
|
|
356591
|
-
if (!dependencies.ok) {
|
|
356592
|
-
return {
|
|
356593
|
-
error: interactiveRunnerMessages.packageJsonUnreadable(dependencies.reason),
|
|
356594
|
-
exitCode: exitCodes.config
|
|
356595
|
-
};
|
|
356596
|
-
}
|
|
356774
|
+
// src/domains/interactiveRunner/highlightSelector.ts
|
|
356775
|
+
async function handleRunnerHighlightSelector(ctx, options, deps) {
|
|
356597
356776
|
const resolved = await resolveRunner(ctx, {
|
|
356598
356777
|
autoLaunch: false,
|
|
356599
|
-
noRunnerIdMessage: interactiveRunnerMessages.
|
|
356778
|
+
noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForHighlight,
|
|
356600
356779
|
runner: options.runner
|
|
356601
356780
|
}, deps);
|
|
356602
356781
|
if (resolved.type === "failed") {
|
|
356603
356782
|
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
356604
356783
|
}
|
|
356605
|
-
const
|
|
356606
|
-
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.
|
|
356607
|
-
id: resolved.runnerId,
|
|
356608
|
-
npmDependencies: dependencies.dependencies,
|
|
356609
|
-
packageName: options.name,
|
|
356610
|
-
packageVersion
|
|
356611
|
-
}, runnerCallOptions);
|
|
356784
|
+
const selector = options.selector ?? "";
|
|
356785
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.highlightSelector, { id: resolved.runnerId, selector }, runnerCallOptions);
|
|
356612
356786
|
if (!result.ok) {
|
|
356613
356787
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
356614
356788
|
}
|
|
356615
|
-
|
|
356616
|
-
|
|
356617
|
-
|
|
356618
|
-
|
|
356619
|
-
|
|
356789
|
+
const { outcome } = result.value;
|
|
356790
|
+
switch (outcome) {
|
|
356791
|
+
case "cleared":
|
|
356792
|
+
ctx.ui.output(result.value, interactiveRunnerMessages.highlightCleared);
|
|
356793
|
+
return;
|
|
356794
|
+
case "success": {
|
|
356795
|
+
const answer = result.value;
|
|
356796
|
+
if (answer.status === "invalid") {
|
|
356620
356797
|
return {
|
|
356621
|
-
error: interactiveRunnerMessages.
|
|
356798
|
+
error: interactiveRunnerMessages.highlightSelectorInvalid(answer.selector),
|
|
356622
356799
|
exitCode: exitCodes.invalidArgs
|
|
356623
356800
|
};
|
|
356624
|
-
case "runner-unreachable":
|
|
356625
|
-
return {
|
|
356626
|
-
error: interactiveRunnerMessages.runnerUnreachable,
|
|
356627
|
-
exitCode: exitCodes.network
|
|
356628
|
-
};
|
|
356629
|
-
default: {
|
|
356630
|
-
return {
|
|
356631
|
-
error: interactiveRunnerMessages.importAnsweredUnknown(failureReason),
|
|
356632
|
-
exitCode: exitCodes.network
|
|
356633
|
-
};
|
|
356634
356801
|
}
|
|
356802
|
+
ctx.ui.output(answer, answer.status === "empty" ? interactiveRunnerMessages.highlightMatchedNothing(answer.selector) : interactiveRunnerMessages.highlightMatched(answer.matchCount, answer.targetPage));
|
|
356803
|
+
return;
|
|
356635
356804
|
}
|
|
356805
|
+
case "failure":
|
|
356806
|
+
return describeFailure(result.value.failureReason);
|
|
356807
|
+
default:
|
|
356808
|
+
return {
|
|
356809
|
+
error: interactiveRunnerMessages.highlightAnsweredUnknown(outcome),
|
|
356810
|
+
exitCode: exitCodes.network
|
|
356811
|
+
};
|
|
356812
|
+
}
|
|
356813
|
+
}
|
|
356814
|
+
function describeFailure(failureReason) {
|
|
356815
|
+
switch (failureReason) {
|
|
356816
|
+
case "no-answer":
|
|
356817
|
+
return {
|
|
356818
|
+
error: interactiveRunnerMessages.highlightNoAnswer,
|
|
356819
|
+
exitCode: exitCodes.network
|
|
356820
|
+
};
|
|
356821
|
+
case "runner-cannot-highlight-selectors":
|
|
356822
|
+
return {
|
|
356823
|
+
error: interactiveRunnerMessages.runnerCannotHighlightSelectors,
|
|
356824
|
+
exitCode: exitCodes.invalidArgs
|
|
356825
|
+
};
|
|
356826
|
+
case "runner-unreachable":
|
|
356827
|
+
return {
|
|
356828
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
356829
|
+
exitCode: exitCodes.network
|
|
356830
|
+
};
|
|
356831
|
+
default:
|
|
356832
|
+
return {
|
|
356833
|
+
error: interactiveRunnerMessages.highlightAnsweredUnknown(failureReason),
|
|
356834
|
+
exitCode: exitCodes.network
|
|
356835
|
+
};
|
|
356636
356836
|
}
|
|
356637
|
-
ctx.ui.output(result.value, interactiveRunnerMessages.packageInstalled(options.name, packageVersion));
|
|
356638
|
-
return;
|
|
356639
356837
|
}
|
|
356640
356838
|
|
|
356641
356839
|
// src/shell/interactiveRunner/collectRunFiles.ts
|
|
356642
|
-
import { join as
|
|
356840
|
+
import { join as join45 } from "node:path";
|
|
356643
356841
|
|
|
356644
356842
|
// src/core/interactiveRunner/getImports.ts
|
|
356645
356843
|
function getImports(options) {
|
|
@@ -356678,14 +356876,14 @@ function isPathAlias(importPath, paths) {
|
|
|
356678
356876
|
}
|
|
356679
356877
|
|
|
356680
356878
|
// src/core/interactiveRunner/resolveImportPath.ts
|
|
356681
|
-
import { dirname as dirname15, join as
|
|
356879
|
+
import { dirname as dirname15, join as join44, normalize as normalize3 } from "node:path/posix";
|
|
356682
356880
|
|
|
356683
356881
|
// src/core/interactiveRunner/tsconfigPaths.ts
|
|
356684
|
-
function
|
|
356882
|
+
function isRecord5(value) {
|
|
356685
356883
|
return typeof value === "object" && value !== null;
|
|
356686
356884
|
}
|
|
356687
356885
|
function isTsconfigPaths(value) {
|
|
356688
|
-
return
|
|
356886
|
+
return isRecord5(value) && Object.values(value).every((targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string"));
|
|
356689
356887
|
}
|
|
356690
356888
|
function parseTsconfigPaths(tsconfigContent) {
|
|
356691
356889
|
let parsed;
|
|
@@ -356694,10 +356892,10 @@ function parseTsconfigPaths(tsconfigContent) {
|
|
|
356694
356892
|
} catch {
|
|
356695
356893
|
return;
|
|
356696
356894
|
}
|
|
356697
|
-
if (!
|
|
356895
|
+
if (!isRecord5(parsed))
|
|
356698
356896
|
return;
|
|
356699
356897
|
const compilerOptions = parsed["compilerOptions"];
|
|
356700
|
-
if (!
|
|
356898
|
+
if (!isRecord5(compilerOptions))
|
|
356701
356899
|
return;
|
|
356702
356900
|
const paths = compilerOptions["paths"];
|
|
356703
356901
|
return isTsconfigPaths(paths) ? paths : undefined;
|
|
@@ -356731,7 +356929,7 @@ function candidatesForExplicitExtension(options) {
|
|
|
356731
356929
|
}
|
|
356732
356930
|
function resolveImportPath(options) {
|
|
356733
356931
|
const aliasTarget = resolvePathAlias(options.importPath, options.tsconfigPaths);
|
|
356734
|
-
const resolvedPath = aliasTarget === undefined ? normalize3(
|
|
356932
|
+
const resolvedPath = aliasTarget === undefined ? normalize3(join44(dirname15(options.importingFilePath), options.importPath)) : normalize3(aliasTarget);
|
|
356735
356933
|
const hasExplicitExtension = supportedExtensions.some((extension) => options.importPath.endsWith(extension));
|
|
356736
356934
|
const candidates = hasExplicitExtension ? candidatesForExplicitExtension({
|
|
356737
356935
|
importPath: options.importPath,
|
|
@@ -356755,7 +356953,7 @@ function resolveImportPath(options) {
|
|
|
356755
356953
|
var tsconfigPath = "tsconfig.json";
|
|
356756
356954
|
async function collectRunFiles(options) {
|
|
356757
356955
|
const held = await listShippablePaths(options);
|
|
356758
|
-
const readFile2 = (path9) => options.fs.readFile(
|
|
356956
|
+
const readFile2 = (path9) => options.fs.readFile(join45(options.cwd, path9));
|
|
356759
356957
|
const tsconfigContent = held.has(tsconfigPath) ? await readFile2(tsconfigPath).catch(() => {
|
|
356760
356958
|
return;
|
|
356761
356959
|
}) : undefined;
|
|
@@ -356838,7 +357036,7 @@ function makeRunnerId() {
|
|
|
356838
357036
|
}
|
|
356839
357037
|
|
|
356840
357038
|
// src/shell/interactiveRunner/runFilesManifest.ts
|
|
356841
|
-
import { join as
|
|
357039
|
+
import { join as join46 } from "node:path";
|
|
356842
357040
|
var manifestFileName = "runner-files.json";
|
|
356843
357041
|
var manifestSchema2 = exports_external.object({
|
|
356844
357042
|
files: exports_external.array(exports_external.object({ contentHash: exports_external.string(), path: exports_external.string() })),
|
|
@@ -356846,8 +357044,8 @@ var manifestSchema2 = exports_external.object({
|
|
|
356846
357044
|
version: exports_external.literal(1)
|
|
356847
357045
|
});
|
|
356848
357046
|
function makeRunFilesManifestStore(options) {
|
|
356849
|
-
const directory =
|
|
356850
|
-
const path9 =
|
|
357047
|
+
const directory = join46(options.cwd, qawolfDir);
|
|
357048
|
+
const path9 = join46(directory, manifestFileName);
|
|
356851
357049
|
let pendingWrites = 0;
|
|
356852
357050
|
return {
|
|
356853
357051
|
async read() {
|
|
@@ -356877,7 +357075,7 @@ function parseJson(text3) {
|
|
|
356877
357075
|
}
|
|
356878
357076
|
|
|
356879
357077
|
// src/shell/interactiveRunner/runnerStore.ts
|
|
356880
|
-
import { join as
|
|
357078
|
+
import { join as join47 } from "node:path";
|
|
356881
357079
|
var storeFileName = "runner.json";
|
|
356882
357080
|
var storeSchema = exports_external.object({ defaultRunnerId: exports_external.string().optional() });
|
|
356883
357081
|
function parseJson2(text3) {
|
|
@@ -356888,8 +357086,8 @@ function parseJson2(text3) {
|
|
|
356888
357086
|
}
|
|
356889
357087
|
}
|
|
356890
357088
|
function makeRunnerStore(options) {
|
|
356891
|
-
const directory =
|
|
356892
|
-
const path9 =
|
|
357089
|
+
const directory = join47(options.cwd, qawolfDir);
|
|
357090
|
+
const path9 = join47(directory, storeFileName);
|
|
356893
357091
|
let pendingWrites = 0;
|
|
356894
357092
|
const nextPendingPath = () => `${path9}.${process.pid}.${++pendingWrites}.tmp`;
|
|
356895
357093
|
return {
|
|
@@ -356971,6 +357169,118 @@ function runnerDeps(ctx) {
|
|
|
356971
357169
|
});
|
|
356972
357170
|
}
|
|
356973
357171
|
|
|
357172
|
+
// src/commands/runner/highlightSelector.register.ts
|
|
357173
|
+
var highlightExamples = `
|
|
357174
|
+
Examples:
|
|
357175
|
+
$ qawolf runner highlight-selector "text=Sign in"
|
|
357176
|
+
$ qawolf runner highlight-selector "#checkout" && qawolf runner screenshot
|
|
357177
|
+
$ qawolf runner highlight-selector`;
|
|
357178
|
+
function registerRunnerHighlightSelectorCommand(runner, signals) {
|
|
357179
|
+
declareCommandKind(runner.command("highlight-selector [selector]"), "write").description("Highlight what a selector matches on a runner's live page, so the next screenshot shows it. Omit the selector to clear the highlight").option("--runner <id>", runnerFlagDescription).addHelpText("after", highlightExamples).action((selector, opts, command) => withAuthContext(signals, (ctx) => handleRunnerHighlightSelector(ctx, { runner: opts.runner, selector }, runnerDeps(ctx)))(opts, command));
|
|
357180
|
+
}
|
|
357181
|
+
|
|
357182
|
+
// src/domains/interactiveRunner/importPackage.ts
|
|
357183
|
+
import { join as join48 } from "node:path";
|
|
357184
|
+
|
|
357185
|
+
// src/core/interactiveRunner/npmDependencies.ts
|
|
357186
|
+
var sectionsInPrecedenceOrder = ["dependencies", "devDependencies"];
|
|
357187
|
+
var isInstallable = (version2) => !version2.startsWith("workspace:");
|
|
357188
|
+
function isRecord6(value) {
|
|
357189
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
357190
|
+
}
|
|
357191
|
+
function readNpmDependencies(packageJsonContent) {
|
|
357192
|
+
let parsed;
|
|
357193
|
+
try {
|
|
357194
|
+
parsed = JSON.parse(packageJsonContent);
|
|
357195
|
+
} catch {
|
|
357196
|
+
return { ok: false, reason: "it is not valid JSON" };
|
|
357197
|
+
}
|
|
357198
|
+
if (!isRecord6(parsed))
|
|
357199
|
+
return { ok: false, reason: "it is not an object" };
|
|
357200
|
+
const dependencies = {};
|
|
357201
|
+
for (const section of sectionsInPrecedenceOrder) {
|
|
357202
|
+
const declared = parsed[section];
|
|
357203
|
+
if (declared === undefined)
|
|
357204
|
+
continue;
|
|
357205
|
+
if (!isRecord6(declared)) {
|
|
357206
|
+
return { ok: false, reason: `${section} is not an object` };
|
|
357207
|
+
}
|
|
357208
|
+
for (const [name, version2] of Object.entries(declared)) {
|
|
357209
|
+
if (typeof version2 !== "string") {
|
|
357210
|
+
return { ok: false, reason: `${section}.${name} is not a string` };
|
|
357211
|
+
}
|
|
357212
|
+
if (Object.hasOwn(dependencies, name))
|
|
357213
|
+
continue;
|
|
357214
|
+
if (isInstallable(version2))
|
|
357215
|
+
dependencies[name] = version2;
|
|
357216
|
+
}
|
|
357217
|
+
}
|
|
357218
|
+
return { dependencies, ok: true };
|
|
357219
|
+
}
|
|
357220
|
+
|
|
357221
|
+
// src/domains/interactiveRunner/importPackage.ts
|
|
357222
|
+
var defaultPackageVersion = "latest";
|
|
357223
|
+
async function handleRunnerImportPackage(ctx, options, deps) {
|
|
357224
|
+
const content = await deps.readFile(join48(deps.cwd, runPackageJsonPath)).catch(() => {
|
|
357225
|
+
return;
|
|
357226
|
+
});
|
|
357227
|
+
if (content === undefined) {
|
|
357228
|
+
return {
|
|
357229
|
+
error: interactiveRunnerMessages.missingPackageJsonForImport,
|
|
357230
|
+
exitCode: exitCodes.config
|
|
357231
|
+
};
|
|
357232
|
+
}
|
|
357233
|
+
const dependencies = readNpmDependencies(content);
|
|
357234
|
+
if (!dependencies.ok) {
|
|
357235
|
+
return {
|
|
357236
|
+
error: interactiveRunnerMessages.packageJsonUnreadable(dependencies.reason),
|
|
357237
|
+
exitCode: exitCodes.config
|
|
357238
|
+
};
|
|
357239
|
+
}
|
|
357240
|
+
const resolved = await resolveRunner(ctx, {
|
|
357241
|
+
autoLaunch: false,
|
|
357242
|
+
noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForImport,
|
|
357243
|
+
runner: options.runner
|
|
357244
|
+
}, deps);
|
|
357245
|
+
if (resolved.type === "failed") {
|
|
357246
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
357247
|
+
}
|
|
357248
|
+
const packageVersion = options.version ?? defaultPackageVersion;
|
|
357249
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.importPackage, {
|
|
357250
|
+
id: resolved.runnerId,
|
|
357251
|
+
npmDependencies: dependencies.dependencies,
|
|
357252
|
+
packageName: options.name,
|
|
357253
|
+
packageVersion
|
|
357254
|
+
}, runnerCallOptions);
|
|
357255
|
+
if (!result.ok) {
|
|
357256
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
357257
|
+
}
|
|
357258
|
+
if (result.value.outcome === "failure") {
|
|
357259
|
+
const failure = result.value;
|
|
357260
|
+
const { failureReason } = failure;
|
|
357261
|
+
switch (failureReason) {
|
|
357262
|
+
case "install-failed":
|
|
357263
|
+
return {
|
|
357264
|
+
error: interactiveRunnerMessages.installFailed(options.name, packageVersion, failure.errorMessage),
|
|
357265
|
+
exitCode: exitCodes.invalidArgs
|
|
357266
|
+
};
|
|
357267
|
+
case "runner-unreachable":
|
|
357268
|
+
return {
|
|
357269
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
357270
|
+
exitCode: exitCodes.network
|
|
357271
|
+
};
|
|
357272
|
+
default: {
|
|
357273
|
+
return {
|
|
357274
|
+
error: interactiveRunnerMessages.importAnsweredUnknown(failureReason),
|
|
357275
|
+
exitCode: exitCodes.network
|
|
357276
|
+
};
|
|
357277
|
+
}
|
|
357278
|
+
}
|
|
357279
|
+
}
|
|
357280
|
+
ctx.ui.output(result.value, interactiveRunnerMessages.packageInstalled(options.name, packageVersion));
|
|
357281
|
+
return;
|
|
357282
|
+
}
|
|
357283
|
+
|
|
356974
357284
|
// src/commands/runner/importPackage.register.ts
|
|
356975
357285
|
var importPackageExamples = `
|
|
356976
357286
|
Examples:
|
|
@@ -357021,6 +357331,11 @@ async function handleRunnerInspect(ctx, options, deps) {
|
|
|
357021
357331
|
error: interactiveRunnerMessages.nothingToInspect(failure.errorMessage),
|
|
357022
357332
|
exitCode: exitCodes.invalidArgs
|
|
357023
357333
|
};
|
|
357334
|
+
case "runner-is-not-a-browser":
|
|
357335
|
+
return {
|
|
357336
|
+
error: interactiveRunnerMessages.inspectNeedsABrowserRunner,
|
|
357337
|
+
exitCode: exitCodes.invalidArgs
|
|
357338
|
+
};
|
|
357024
357339
|
case "runner-unreachable":
|
|
357025
357340
|
return {
|
|
357026
357341
|
error: interactiveRunnerMessages.runnerUnreachable,
|
|
@@ -357238,6 +357553,50 @@ function parseBrowserAction(candidate) {
|
|
|
357238
357553
|
return { action: parsed.data, ok: true };
|
|
357239
357554
|
}
|
|
357240
357555
|
|
|
357556
|
+
// src/domains/interactiveRunner/performActionFailure.ts
|
|
357557
|
+
function describePerformActionFailure(options) {
|
|
357558
|
+
const { actionType, failure } = options;
|
|
357559
|
+
const { failureReason } = failure;
|
|
357560
|
+
switch (failureReason) {
|
|
357561
|
+
case "action-failed":
|
|
357562
|
+
return {
|
|
357563
|
+
error: interactiveRunnerMessages.actionFailed(failure.errorMessage),
|
|
357564
|
+
exitCode: exitCodes.testFailure
|
|
357565
|
+
};
|
|
357566
|
+
case "action-not-supported-on-mobile":
|
|
357567
|
+
return {
|
|
357568
|
+
error: interactiveRunnerMessages.actionNotSupportedOnMobile(actionType),
|
|
357569
|
+
exitCode: exitCodes.invalidArgs
|
|
357570
|
+
};
|
|
357571
|
+
case "screen-needs-a-run":
|
|
357572
|
+
return {
|
|
357573
|
+
error: interactiveRunnerMessages.screenNeedsARun,
|
|
357574
|
+
exitCode: exitCodes.invalidArgs
|
|
357575
|
+
};
|
|
357576
|
+
case "screen-not-ready":
|
|
357577
|
+
return {
|
|
357578
|
+
error: interactiveRunnerMessages.screenNotReady,
|
|
357579
|
+
exitCode: exitCodes.network
|
|
357580
|
+
};
|
|
357581
|
+
case "runner-has-no-screen":
|
|
357582
|
+
return {
|
|
357583
|
+
error: interactiveRunnerMessages.runnerHasNoScreen,
|
|
357584
|
+
exitCode: exitCodes.invalidArgs
|
|
357585
|
+
};
|
|
357586
|
+
case "runner-unreachable":
|
|
357587
|
+
return {
|
|
357588
|
+
error: interactiveRunnerMessages.actionMayHaveHappened,
|
|
357589
|
+
exitCode: exitCodes.network
|
|
357590
|
+
};
|
|
357591
|
+
default: {
|
|
357592
|
+
return {
|
|
357593
|
+
error: interactiveRunnerMessages.actionAnsweredUnknown(failureReason),
|
|
357594
|
+
exitCode: exitCodes.network
|
|
357595
|
+
};
|
|
357596
|
+
}
|
|
357597
|
+
}
|
|
357598
|
+
}
|
|
357599
|
+
|
|
357241
357600
|
// src/domains/interactiveRunner/performAction.ts
|
|
357242
357601
|
var stdinArgument2 = "-";
|
|
357243
357602
|
async function readAction(type, flags, deps) {
|
|
@@ -357280,41 +357639,10 @@ async function handleRunnerAct(ctx, options, deps) {
|
|
|
357280
357639
|
ctx.ui.output({ action: built.action, outcome: "success" }, interactiveRunnerMessages.actionPerformed(built.action.type));
|
|
357281
357640
|
return;
|
|
357282
357641
|
}
|
|
357283
|
-
|
|
357284
|
-
|
|
357285
|
-
|
|
357286
|
-
|
|
357287
|
-
return {
|
|
357288
|
-
error: interactiveRunnerMessages.actionFailed(failure.errorMessage),
|
|
357289
|
-
exitCode: exitCodes.testFailure
|
|
357290
|
-
};
|
|
357291
|
-
case "screen-needs-a-run":
|
|
357292
|
-
return {
|
|
357293
|
-
error: interactiveRunnerMessages.screenNeedsARun,
|
|
357294
|
-
exitCode: exitCodes.invalidArgs
|
|
357295
|
-
};
|
|
357296
|
-
case "screen-not-ready":
|
|
357297
|
-
return {
|
|
357298
|
-
error: interactiveRunnerMessages.screenNotReady,
|
|
357299
|
-
exitCode: exitCodes.network
|
|
357300
|
-
};
|
|
357301
|
-
case "runner-has-no-screen":
|
|
357302
|
-
return {
|
|
357303
|
-
error: interactiveRunnerMessages.runnerHasNoScreen,
|
|
357304
|
-
exitCode: exitCodes.invalidArgs
|
|
357305
|
-
};
|
|
357306
|
-
case "runner-unreachable":
|
|
357307
|
-
return {
|
|
357308
|
-
error: interactiveRunnerMessages.actionMayHaveHappened,
|
|
357309
|
-
exitCode: exitCodes.network
|
|
357310
|
-
};
|
|
357311
|
-
default: {
|
|
357312
|
-
return {
|
|
357313
|
-
error: interactiveRunnerMessages.actionAnsweredUnknown(failureReason),
|
|
357314
|
-
exitCode: exitCodes.network
|
|
357315
|
-
};
|
|
357316
|
-
}
|
|
357317
|
-
}
|
|
357642
|
+
return describePerformActionFailure({
|
|
357643
|
+
actionType: built.action.type,
|
|
357644
|
+
failure: result.value
|
|
357645
|
+
});
|
|
357318
357646
|
}
|
|
357319
357647
|
|
|
357320
357648
|
// src/domains/interactiveRunner/takeScreenshot.ts
|
|
@@ -357528,6 +357856,67 @@ function registerRunnerLifecycleCommands(runner, signals) {
|
|
|
357528
357856
|
declareCommandKind(runner.command("keepalive"), "read").description("Reset a runner's inactivity clock, for a caller that pauses between actions").option("--runner <id>", runnerFlagDescription).addHelpText("after", keepaliveExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerKeepalive(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
357529
357857
|
}
|
|
357530
357858
|
|
|
357859
|
+
// src/domains/interactiveRunner/promoteSnapshot.ts
|
|
357860
|
+
async function handleRunnerPromoteSnapshot(ctx, options, deps) {
|
|
357861
|
+
const resolved = await resolveRunner(ctx, {
|
|
357862
|
+
autoLaunch: false,
|
|
357863
|
+
noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForPromoteSnapshot,
|
|
357864
|
+
runner: options.runner
|
|
357865
|
+
}, deps);
|
|
357866
|
+
if (resolved.type === "failed") {
|
|
357867
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
357868
|
+
}
|
|
357869
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.promoteSnapshot, {
|
|
357870
|
+
baselinePath: options.baselinePath,
|
|
357871
|
+
id: resolved.runnerId,
|
|
357872
|
+
screenshotPath: options.screenshotPath
|
|
357873
|
+
}, runnerCallOptions);
|
|
357874
|
+
if (!result.ok) {
|
|
357875
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
357876
|
+
}
|
|
357877
|
+
if (result.value.outcome === "success") {
|
|
357878
|
+
ctx.ui.output(result.value, interactiveRunnerMessages.snapshotPromoted(options.screenshotPath, options.baselinePath));
|
|
357879
|
+
return;
|
|
357880
|
+
}
|
|
357881
|
+
const { failureReason } = result.value;
|
|
357882
|
+
switch (failureReason) {
|
|
357883
|
+
case "snapshot-not-found":
|
|
357884
|
+
return {
|
|
357885
|
+
error: interactiveRunnerMessages.snapshotNotFound(options.screenshotPath),
|
|
357886
|
+
exitCode: exitCodes.invalidArgs
|
|
357887
|
+
};
|
|
357888
|
+
case "runner-cannot-promote-snapshots":
|
|
357889
|
+
return {
|
|
357890
|
+
error: interactiveRunnerMessages.runnerCannotPromoteSnapshots,
|
|
357891
|
+
exitCode: exitCodes.invalidArgs
|
|
357892
|
+
};
|
|
357893
|
+
case "runner-unreachable":
|
|
357894
|
+
return {
|
|
357895
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
357896
|
+
exitCode: exitCodes.network
|
|
357897
|
+
};
|
|
357898
|
+
default: {
|
|
357899
|
+
return {
|
|
357900
|
+
error: interactiveRunnerMessages.promoteSnapshotAnsweredUnknown(failureReason),
|
|
357901
|
+
exitCode: exitCodes.network
|
|
357902
|
+
};
|
|
357903
|
+
}
|
|
357904
|
+
}
|
|
357905
|
+
}
|
|
357906
|
+
|
|
357907
|
+
// src/commands/runner/promoteSnapshot.register.ts
|
|
357908
|
+
var promoteSnapshotExamples = `
|
|
357909
|
+
Examples:
|
|
357910
|
+
$ qawolf runner promote-snapshot --screenshot checkout-1-actual.png --baseline checkout-1.png
|
|
357911
|
+
$ qawolf runner events run-events --tail 20 | jq 'select(.type == "image-diff-artifact")'`;
|
|
357912
|
+
function registerRunnerPromoteSnapshotCommand(runner, signals) {
|
|
357913
|
+
declareCommandKind(runner.command("promote-snapshot"), "write").description("Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it").requiredOption("--screenshot <path>", "The screenshot to promote, as the image diff named it").requiredOption("--baseline <path>", "The baseline to replace, as the image diff named it").option("--runner <id>", runnerFlagDescription).addHelpText("after", promoteSnapshotExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerPromoteSnapshot(ctx, {
|
|
357914
|
+
baselinePath: opts.baseline,
|
|
357915
|
+
runner: opts.runner,
|
|
357916
|
+
screenshotPath: opts.screenshot
|
|
357917
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357918
|
+
}
|
|
357919
|
+
|
|
357531
357920
|
// src/core/interactiveRunner/followTimeout.ts
|
|
357532
357921
|
var defaultFollowTimeoutSeconds = 3600;
|
|
357533
357922
|
var followTimeoutSchema = exports_external.coerce.number().int().positive();
|
|
@@ -357916,6 +358305,13 @@ var refused = (error51, exitCode) => ({
|
|
|
357916
358305
|
ok: false
|
|
357917
358306
|
});
|
|
357918
358307
|
async function prepareRun(options, deps) {
|
|
358308
|
+
const environmentId = options.envId?.trim();
|
|
358309
|
+
if (environmentId !== undefined && options.envFile !== undefined) {
|
|
358310
|
+
return refused(interactiveRunnerMessages.envIdWithEnvFile, exitCodes.invalidArgs);
|
|
358311
|
+
}
|
|
358312
|
+
if (environmentId === "") {
|
|
358313
|
+
return refused(interactiveRunnerMessages.envIdBlank, exitCodes.invalidArgs);
|
|
358314
|
+
}
|
|
357919
358315
|
const linesFilePath = options.linesFile === undefined ? undefined : toCollectedPath(deps.cwd, options.linesFile);
|
|
357920
358316
|
const roots = linesFilePath === undefined || linesFilePath === options.entryPointPath ? [options.entryPointPath] : [options.entryPointPath, linesFilePath];
|
|
357921
358317
|
const collected = await collectRunFiles2(deps, roots);
|
|
@@ -357932,14 +358328,26 @@ async function prepareRun(options, deps) {
|
|
|
357932
358328
|
}
|
|
357933
358329
|
const given = environment3?.ok === true ? environment3.environment : undefined;
|
|
357934
358330
|
if (options.lines === undefined) {
|
|
357935
|
-
return options.linesFile === undefined ? {
|
|
358331
|
+
return options.linesFile === undefined ? {
|
|
358332
|
+
environment: given,
|
|
358333
|
+
environmentId,
|
|
358334
|
+
files,
|
|
358335
|
+
ok: true,
|
|
358336
|
+
selection: undefined
|
|
358337
|
+
} : refused(interactiveRunnerMessages.linesFileWithoutLines, exitCodes.invalidArgs);
|
|
357936
358338
|
}
|
|
357937
358339
|
const path9 = linesFilePath ?? options.entryPointPath;
|
|
357938
358340
|
if (!Object.hasOwn(files, path9)) {
|
|
357939
358341
|
return refused(interactiveRunnerMessages.fileNotCollected(path9), exitCodes.invalidArgs);
|
|
357940
358342
|
}
|
|
357941
358343
|
const built = buildRunSelection({ lines: options.lines, path: path9 });
|
|
357942
|
-
return built.ok ? {
|
|
358344
|
+
return built.ok ? {
|
|
358345
|
+
environment: given,
|
|
358346
|
+
environmentId,
|
|
358347
|
+
files,
|
|
358348
|
+
ok: true,
|
|
358349
|
+
selection: built.selection
|
|
358350
|
+
} : refused(built.error, exitCodes.invalidArgs);
|
|
357943
358351
|
}
|
|
357944
358352
|
async function readEnvironment(envFile, deps) {
|
|
357945
358353
|
if (envFile === undefined)
|
|
@@ -357969,6 +358377,9 @@ function buildRunFileDelta(options) {
|
|
|
357969
358377
|
}
|
|
357970
358378
|
const heldHashes = new Map(held.files.map((entry) => [entry.path, entry.contentHash]));
|
|
357971
358379
|
const alwaysSent = new Set([options.entryPointPath, runPackageJsonPath]);
|
|
358380
|
+
if (options.selectionPath !== undefined) {
|
|
358381
|
+
alwaysSent.add(options.selectionPath);
|
|
358382
|
+
}
|
|
357972
358383
|
const files = {};
|
|
357973
358384
|
const unchangedFiles = {};
|
|
357974
358385
|
for (const [path9, content] of Object.entries(options.files)) {
|
|
@@ -358017,6 +358428,7 @@ async function sendRunFlowRequest(ctx, options, delta) {
|
|
|
358017
358428
|
files: delta.files,
|
|
358018
358429
|
id: options.resolved.runnerId,
|
|
358019
358430
|
...options.environment === undefined ? {} : { env: options.environment },
|
|
358431
|
+
...options.environmentId === undefined ? {} : { environmentId: options.environmentId },
|
|
358020
358432
|
...options.selection === undefined ? {} : { selection: options.selection },
|
|
358021
358433
|
...delta.unchangedFiles === undefined ? {} : { unchangedFiles: delta.unchangedFiles }
|
|
358022
358434
|
}, runnerCallOptions);
|
|
@@ -358056,7 +358468,8 @@ async function submitRun(ctx, options, deps) {
|
|
|
358056
358468
|
entryPointPath: options.entryPointPath,
|
|
358057
358469
|
files: options.files,
|
|
358058
358470
|
held,
|
|
358059
|
-
runnerId: options.resolved.runnerId
|
|
358471
|
+
runnerId: options.resolved.runnerId,
|
|
358472
|
+
selectionPath: options.selection?.path
|
|
358060
358473
|
});
|
|
358061
358474
|
const first2 = await sendRunFlowRequest(ctx, options, delta);
|
|
358062
358475
|
if (first2.type === "needs-full-sync") {
|
|
@@ -358102,6 +358515,7 @@ async function handleRunnerRun(ctx, options, deps) {
|
|
|
358102
358515
|
const prepared = await prepareRun({
|
|
358103
358516
|
entryPointPath,
|
|
358104
358517
|
envFile: options.envFile,
|
|
358518
|
+
envId: options.envId,
|
|
358105
358519
|
lines: options.lines,
|
|
358106
358520
|
linesFile: options.linesFile
|
|
358107
358521
|
}, deps);
|
|
@@ -358123,6 +358537,7 @@ async function handleRunnerRun(ctx, options, deps) {
|
|
|
358123
358537
|
const submitted = await submitRun(ctx, {
|
|
358124
358538
|
entryPointPath,
|
|
358125
358539
|
environment: prepared.environment,
|
|
358540
|
+
environmentId: prepared.environmentId,
|
|
358126
358541
|
files: prepared.files,
|
|
358127
358542
|
resolved,
|
|
358128
358543
|
selection: prepared.selection
|
|
@@ -358161,11 +358576,13 @@ Examples:
|
|
|
358161
358576
|
$ qawolf runner run flows/checkout.flow.ts --follow
|
|
358162
358577
|
$ qawolf runner run flows/checkout.flow.ts --follow --logs
|
|
358163
358578
|
$ qawolf runner run flows/checkout.flow.ts --lines 12-40
|
|
358164
|
-
$ qawolf runner run flows/checkout.flow.ts --lines 4-9 --lines-file pages/login.ts
|
|
358579
|
+
$ qawolf runner run flows/checkout.flow.ts --lines 4-9 --lines-file pages/login.ts
|
|
358580
|
+
$ qawolf runner run flows/checkout.flow.ts --env-id staging`;
|
|
358165
358581
|
function registerRunCommand(runner, signals) {
|
|
358166
|
-
declareCommandKind(runner.command("run <flowFile>"), "write").description("Run a flow on an interactive runner, shipping the flow and what it imports").option("--follow", "Report the run's status until it settles: in progress, then passed or failed", false).option("--logs", "Stream every log line the run produces while following. Implies --follow", false).option("--run-events", "Stream the run's progress events as JSON lines while following. Implies --follow", false).option("--recorder-events", "Stream the browser actions the runner records as JSON lines while following, from an anchor taken just before submission: the recorder is runner-wide, not run-scoped. Implies --follow", false).option("--env-
|
|
358582
|
+
declareCommandKind(runner.command("run <flowFile>"), "write").description("Run a flow on an interactive runner, shipping the flow and what it imports").option("--follow", "Report the run's status until it settles: in progress, then passed or failed", false).option("--logs", "Stream every log line the run produces while following. Implies --follow", false).option("--run-events", "Stream the run's progress events as JSON lines while following. Implies --follow", false).option("--recorder-events", "Stream the browser actions the runner records as JSON lines while following, from an anchor taken just before submission: the recorder is runner-wide, not run-scoped. Implies --follow", false).option("--env-id <env>", "QA Wolf environment whose variables the run is given, by id or alias. QA Wolf reads and decrypts them itself, so they never leave the server and no size limit applies to them").option("--env-file <path>", "Dotenv file on this machine whose variables the run is given. Pass this or --env-id, not both").option("--lines <start-end>", "Run only these 1-indexed lines against the browser as it stands, instead of the whole flow from a fresh one").option("--lines-file <path>", "File the --lines range lives in. Defaults to <flowFile>; pass it only when the lines are in another file, such as a page object").option("--runner <id>", runnerFlagDescription).option("--timeout <seconds>", "Give up following after this long. Following keeps the runner alive, so a run that never settles would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", runExamples2).action((flowFile, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRun(ctx, {
|
|
358167
358583
|
entryPoint: flowFile,
|
|
358168
358584
|
envFile: opts.envFile,
|
|
358585
|
+
envId: opts.envId,
|
|
358169
358586
|
follow: opts.follow,
|
|
358170
358587
|
lines: opts.lines,
|
|
358171
358588
|
linesFile: opts.linesFile,
|
|
@@ -358186,6 +358603,8 @@ function registerRunnerCommand(program2, signals) {
|
|
|
358186
358603
|
registerRunnerInteractCommands(runner, signals);
|
|
358187
358604
|
registerRunnerInspectCommands(runner, signals);
|
|
358188
358605
|
registerRunnerImportPackageCommand(runner, signals);
|
|
358606
|
+
registerRunnerHighlightSelectorCommand(runner, signals);
|
|
358607
|
+
registerRunnerPromoteSnapshotCommand(runner, signals);
|
|
358189
358608
|
}
|
|
358190
358609
|
|
|
358191
358610
|
// src/commands/program.ts
|
|
@@ -358224,4 +358643,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
358224
358643
|
process.exitCode = 1;
|
|
358225
358644
|
}).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
358226
358645
|
|
|
358227
|
-
//# debugId=
|
|
358646
|
+
//# debugId=408E9965E881500264756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qawolf/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@clack/prompts": "1.5.1",
|
|
61
61
|
"@napi-rs/keyring": "1.3.0",
|
|
62
62
|
"@oxc-node/core": "0.1.0",
|
|
63
|
-
"@qawolf/api-contracts": "0.
|
|
63
|
+
"@qawolf/api-contracts": "0.34.0",
|
|
64
64
|
"@qawolf/emails": "1.1.1",
|
|
65
65
|
"@qawolf/flow-targets": "1.0.0",
|
|
66
66
|
"@qawolf/flows": "0.1.4",
|
|
@@ -145,12 +145,14 @@ current branch.
|
|
|
145
145
|
| `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 |
|
|
146
146
|
| `qawolf runner events` | read | Print a runner's journal, one entry per line. QA Wolf writes console, recorder, run-events, run-logs, run-status |
|
|
147
147
|
| `qawolf runner exec` | write | Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin |
|
|
148
|
+
| `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 |
|
|
148
149
|
| `qawolf runner import-package` | write | Install a package into a runner's live run, so a snippet or a selection can import it |
|
|
149
150
|
| `qawolf runner inspect element-html` | read | Print the HTML of the first element a selector matches |
|
|
150
151
|
| `qawolf runner inspect page-html` | read | Print the page's HTML, simplified for a model to read |
|
|
151
152
|
| `qawolf runner inspect variable` | read | Print a top-level variable's value from the running workflow |
|
|
152
153
|
| `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
|
|
153
154
|
| `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
|
|
155
|
+
| `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
|
|
154
156
|
| `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
|
|
155
157
|
| `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |
|
|
156
158
|
| `qawolf runner stop-run` | write | Stop what a runner is currently executing, leaving the runner up |
|
|
@@ -159,6 +159,53 @@ Use `inspect` before reaching for `exec`. Reading a value through a snippet
|
|
|
159
159
|
means printing it and then fishing it back out of the `console` stream, which is
|
|
160
160
|
two calls and a marker; `inspect variable` is one call and the value.
|
|
161
161
|
|
|
162
|
+
## Checking a selector: `highlight-selector`
|
|
163
|
+
|
|
164
|
+
`inspect element-html` tells you what a selector matched. `highlight-selector`
|
|
165
|
+
shows you _where_ it is, by drawing on the page itself:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
qawolf runner highlight-selector "text=Sign in"
|
|
169
|
+
qawolf runner screenshot # the highlight is in this
|
|
170
|
+
qawolf runner highlight-selector # omit the selector to clear
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The highlight stays until it is replaced or cleared, which is the point: you
|
|
174
|
+
cannot see the runner's screen, so the only way to read the result is the next
|
|
175
|
+
screenshot.
|
|
176
|
+
|
|
177
|
+
Three answers are worth telling apart. A selector that matched prints how many
|
|
178
|
+
elements it hit and exits `0`. A selector the page read fine but that matched
|
|
179
|
+
nothing also exits `0`, because the call did what was asked and the count is the
|
|
180
|
+
answer; the message says the syntax was fine so you look at the page, not the
|
|
181
|
+
locator. A selector the page could not read at all exits `2`, because that one
|
|
182
|
+
is yours to correct and retrying will not change it.
|
|
183
|
+
|
|
184
|
+
`runner-cannot-highlight-selectors` exits `2` and means the runner has no
|
|
185
|
+
browser to draw on. `no-answer` exits `4`: a highlight runs inside the page, so
|
|
186
|
+
a page that is gone or mid-navigation does not answer at all rather than
|
|
187
|
+
answering slowly.
|
|
188
|
+
|
|
189
|
+
## Accepting a new baseline: `promote-snapshot`
|
|
190
|
+
|
|
191
|
+
When a run's image diff fails and the new screenshot is the one you want, this
|
|
192
|
+
replaces the baseline on the runner that produced it:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
qawolf runner events run-events --tail 20 | jq 'select(.type == "image-diff-artifact")'
|
|
196
|
+
qawolf runner promote-snapshot --screenshot checkout-1-actual.png --baseline checkout-1.png
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Both paths are the ones the diff reported, and both are named rather than
|
|
200
|
+
positional, because two paths with one unlabelled is easy to get backwards and
|
|
201
|
+
swapping them promotes the wrong image. They are paths inside the run's own
|
|
202
|
+
screenshot storage, not files on your machine.
|
|
203
|
+
|
|
204
|
+
`snapshot-not-found` exits `2` and means the run wrote no screenshot at that
|
|
205
|
+
path, which nearly always means the paths did not come from a diff this run
|
|
206
|
+
produced. Nothing is changed, so correcting the path and repeating is safe.
|
|
207
|
+
Promoting twice is also safe, so an unreachable runner is worth retrying.
|
|
208
|
+
|
|
162
209
|
## Installing a package mid-session
|
|
163
210
|
|
|
164
211
|
`qawolf runner import-package <name>` installs a package into the runner's live
|
|
@@ -255,14 +302,29 @@ runner is addressed, naming the path.
|
|
|
255
302
|
|
|
256
303
|
### Giving the run environment variables
|
|
257
304
|
|
|
305
|
+
There are two ways, and a run takes one of them. `--env-id` names a QA Wolf
|
|
306
|
+
environment by id or alias, the same reference `qawolf flows` takes as `--env`.
|
|
258
307
|
`--env-file .env` gives the run the variables in a dotenv file, in the format
|
|
259
|
-
`qawolf flows pull` writes.
|
|
260
|
-
|
|
308
|
+
`qawolf flows pull` writes.
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
qawolf runner run flows/checkout.flow.ts --env-id staging
|
|
312
|
+
qawolf runner run flows/checkout.flow.ts --env-file .env
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Prefer `--env-id`.** QA Wolf reads and decrypts the environment itself, so the
|
|
316
|
+
values never leave the server, nothing has to be pulled to disk first, and no
|
|
317
|
+
size limit applies to them. It is the only way to run a flow whose environment
|
|
318
|
+
holds something large, such as a session cookie.
|
|
319
|
+
|
|
320
|
+
A run that sends its own variables with `--env-file` may carry at most 200 of
|
|
321
|
+
them, each value at most 16 KiB. Names follow what a shell accepts, and
|
|
322
|
+
`QAWOLF_TEAM_ID` is reserved because QA Wolf sets it from the key you
|
|
323
|
+
authenticated with. All of that is refused before a runner is addressed, naming
|
|
324
|
+
the variable at fault.
|
|
261
325
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
from the key you authenticated with. All of that is refused before a runner is
|
|
265
|
-
addressed, naming the variable at fault.
|
|
326
|
+
Passing both flags is refused. They each give the run its whole environment, so
|
|
327
|
+
there is no order in which they would combine.
|
|
266
328
|
|
|
267
329
|
If the runner had no browser, one is started before your lines run, and the
|
|
268
330
|
command says so on stderr. Those lines then ran against a fresh page rather than
|