@qawolf/cli 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -326107,6 +326107,8 @@ var interactMessages = {
326107
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.",
326108
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.`,
326109
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.`,
326110
+ 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.`,
326111
+ 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
326112
  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
326113
  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
326114
  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.",
@@ -342551,7 +342553,6 @@ var makeInspectMobileOnRunnerContract = () => {
342551
342553
  output
342552
342554
  };
342553
342555
  };
342554
-
342555
342556
  // node_modules/@qawolf/api-contracts/dist/v1/runner/journal.js
342556
342557
  var journalEntrySchema = exports_external.object({
342557
342558
  payload: exports_external.unknown(),
@@ -346126,7 +346127,7 @@ function startUpdateCheck(deps) {
346126
346127
  // package.json
346127
346128
  var package_default = {
346128
346129
  name: "@qawolf/cli",
346129
- version: "1.18.0",
346130
+ version: "1.19.0",
346130
346131
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
346131
346132
  keywords: [
346132
346133
  "automation",
@@ -357446,14 +357447,190 @@ async function handleRunnerInspect(ctx, options, deps) {
357446
357447
  return;
357447
357448
  }
357448
357449
 
357450
+ // src/core/interactiveRunner/inspectMobileRequest.ts
357451
+ function toNumber(value) {
357452
+ return value.trim() === "" ? Number.NaN : Number(value);
357453
+ }
357454
+ function buildInspectMobileRequest(what, flags) {
357455
+ if (flags.by === "point" && (flags.text !== undefined || flags.partial !== undefined)) {
357456
+ return {
357457
+ 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.",
357458
+ ok: false
357459
+ };
357460
+ }
357461
+ if (flags.by === "text" && (flags.x !== undefined || flags.y !== undefined)) {
357462
+ return {
357463
+ 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.",
357464
+ ok: false
357465
+ };
357466
+ }
357467
+ const candidate = {
357468
+ what,
357469
+ ...flags.by === undefined ? {} : { by: flags.by },
357470
+ ...flags.context === undefined ? {} : { context: flags.context },
357471
+ ...flags.partial === undefined ? {} : { partial: flags.partial },
357472
+ ...flags.text === undefined ? {} : { text: flags.text },
357473
+ ...flags.x === undefined ? {} : { x: toNumber(flags.x) },
357474
+ ...flags.y === undefined ? {} : { y: toNumber(flags.y) }
357475
+ };
357476
+ const parsed = inspectMobileRequestSchema.safeParse(candidate);
357477
+ if (!parsed.success) {
357478
+ return { error: exports_external.prettifyError(parsed.error), ok: false };
357479
+ }
357480
+ return { ok: true, request: parsed.data };
357481
+ }
357482
+
357483
+ // src/domains/interactiveRunner/inspectMobile.ts
357484
+ function describeSession(session) {
357485
+ switch (session.type) {
357486
+ case "ready":
357487
+ return session.deviceName === undefined ? `Session ready: ${session.platformName} (${session.sessionId}).` : `Session ready: ${session.platformName} on ${session.deviceName} (${session.sessionId}).`;
357488
+ case "unreachable":
357489
+ return `Session unreachable: ${session.error}`;
357490
+ case "ambiguous":
357491
+ return `${String(session.sessionCount)} Appium sessions are live; expected one.`;
357492
+ case "no-session":
357493
+ return "No Appium session is live.";
357494
+ }
357495
+ }
357496
+ function streamLine(value) {
357497
+ switch (value.what) {
357498
+ case "contexts":
357499
+ return JSON.stringify({
357500
+ contexts: value.contexts,
357501
+ current: value.current
357502
+ });
357503
+ case "page":
357504
+ return JSON.stringify({
357505
+ context: value.context,
357506
+ orientation: value.orientation,
357507
+ pageSource: value.pageSource
357508
+ });
357509
+ case "elements":
357510
+ return JSON.stringify({ matches: value.matches });
357511
+ }
357512
+ }
357513
+ async function handleRunnerInspectMobile(ctx, options, deps) {
357514
+ const built = buildInspectMobileRequest(options.what, options.flags);
357515
+ if (!built.ok)
357516
+ return { error: built.error, exitCode: exitCodes.invalidArgs };
357517
+ const resolved = await resolveRunner(ctx, {
357518
+ autoLaunch: false,
357519
+ noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForInspect,
357520
+ runner: options.runner
357521
+ }, deps);
357522
+ if (resolved.type === "failed") {
357523
+ return { ...failureFields(resolved), exitCode: resolved.exitCode };
357524
+ }
357525
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.inspectMobile, { id: resolved.runnerId, request: built.request }, runnerCallOptions);
357526
+ if (!result.ok) {
357527
+ return { ...failureFields(result), exitCode: exitCodes.network };
357528
+ }
357529
+ if (result.value.outcome === "success") {
357530
+ const value = result.value;
357531
+ if (value.what === "session") {
357532
+ ctx.ui.output(value, describeSession(value.session));
357533
+ } else {
357534
+ ctx.ui.stream(value, streamLine(value));
357535
+ }
357536
+ return;
357537
+ }
357538
+ const { failureReason } = result.value;
357539
+ switch (failureReason) {
357540
+ case "runner-is-not-mobile":
357541
+ return {
357542
+ error: interactiveRunnerMessages.runnerIsNotMobile,
357543
+ exitCode: exitCodes.invalidArgs
357544
+ };
357545
+ case "screen-needs-a-run":
357546
+ return {
357547
+ error: interactiveRunnerMessages.screenNeedsARun,
357548
+ exitCode: exitCodes.invalidArgs
357549
+ };
357550
+ case "screen-not-ready":
357551
+ return {
357552
+ error: interactiveRunnerMessages.screenNotReady,
357553
+ exitCode: exitCodes.network
357554
+ };
357555
+ case "runner-unreachable":
357556
+ return {
357557
+ error: interactiveRunnerMessages.runnerUnreachable,
357558
+ exitCode: exitCodes.network
357559
+ };
357560
+ default: {
357561
+ return {
357562
+ error: interactiveRunnerMessages.inspectMobileAnsweredUnknown(failureReason),
357563
+ exitCode: exitCodes.network
357564
+ };
357565
+ }
357566
+ }
357567
+ }
357568
+
357569
+ // src/commands/runner/inspectMobile.register.ts
357570
+ function registerRunnerInspectMobileCommands(inspect, signals) {
357571
+ 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, {
357572
+ flags: {
357573
+ by: undefined,
357574
+ context: undefined,
357575
+ partial: undefined,
357576
+ text: undefined,
357577
+ x: undefined,
357578
+ y: undefined
357579
+ },
357580
+ runner: opts.runner,
357581
+ what: "session"
357582
+ }, runnerDeps(ctx)))(opts, command));
357583
+ 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, {
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: "contexts"
357594
+ }, runnerDeps(ctx)))(opts, command));
357595
+ 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, {
357596
+ flags: {
357597
+ by: undefined,
357598
+ context: opts.context,
357599
+ partial: undefined,
357600
+ text: undefined,
357601
+ x: undefined,
357602
+ y: undefined
357603
+ },
357604
+ runner: opts.runner,
357605
+ what: "page"
357606
+ }, runnerDeps(ctx)))(opts, command));
357607
+ 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, {
357608
+ flags: {
357609
+ by: opts.by,
357610
+ context: opts.context,
357611
+ partial: opts.partial,
357612
+ text: opts.text,
357613
+ x: opts.x,
357614
+ y: opts.y
357615
+ },
357616
+ runner: opts.runner,
357617
+ what: "elements"
357618
+ }, runnerDeps(ctx)))(opts, command));
357619
+ }
357620
+
357449
357621
  // src/commands/runner/inspect.register.ts
357450
357622
  var inspectExamples = `
357451
357623
  Examples:
357452
357624
  $ qawolf runner inspect element-html --selector "#email"
357453
357625
  $ qawolf runner inspect page-html > page.html
357454
- $ qawolf runner inspect variable --name cart | jq .total`;
357626
+ $ qawolf runner inspect variable --name cart | jq .total
357627
+ $ qawolf runner inspect session
357628
+ $ qawolf runner inspect contexts
357629
+ $ qawolf runner inspect page-source --context WEBVIEW_1
357630
+ $ qawolf runner inspect elements --by point --x 200 --y 400
357631
+ $ qawolf runner inspect elements --by text --text "Sign in" --partial`;
357455
357632
  function registerRunnerInspectCommands(runner, signals) {
357456
- const inspect = runner.command("inspect").description("Read one thing off a runner's live page").addHelpText("after", inspectExamples);
357633
+ const inspect = runner.command("inspect").description("Read one thing off a runner's live page (browser) or Appium session (mobile)").addHelpText("after", inspectExamples);
357457
357634
  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
357635
  flags: { name: undefined, selector: opts.selector },
357459
357636
  runner: opts.runner,
@@ -357469,6 +357646,7 @@ function registerRunnerInspectCommands(runner, signals) {
357469
357646
  runner: opts.runner,
357470
357647
  what: "variable"
357471
357648
  }, runnerDeps(ctx)))(opts, command));
357649
+ registerRunnerInspectMobileCommands(inspect, signals);
357472
357650
  }
357473
357651
 
357474
357652
  // src/core/interactiveRunner/runFiles.ts
@@ -357606,46 +357784,6 @@ async function handleRunnerExec(ctx, options, deps) {
357606
357784
  }
357607
357785
  }
357608
357786
 
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
357787
  // src/domains/interactiveRunner/performActionFailure.ts
357650
357788
  function describePerformActionFailure(options) {
357651
357789
  const { actionType, failure } = options;
@@ -357690,7 +357828,47 @@ function describePerformActionFailure(options) {
357690
357828
  }
357691
357829
  }
357692
357830
 
357693
- // src/domains/interactiveRunner/performAction.ts
357831
+ // src/core/interactiveRunner/browserAction.ts
357832
+ function parseJsonPath(path9) {
357833
+ try {
357834
+ return { ok: true, value: JSON.parse(path9) };
357835
+ } catch {
357836
+ return {
357837
+ error: `--path must be a JSON array of points, for example '[{"x":10,"y":20},{"x":80,"y":90}]'.`,
357838
+ ok: false
357839
+ };
357840
+ }
357841
+ }
357842
+ function toNumber2(value) {
357843
+ return value.trim() === "" ? Number.NaN : Number(value);
357844
+ }
357845
+ function buildBrowserAction(type, flags) {
357846
+ const parsedPath = flags.path === undefined ? undefined : parseJsonPath(flags.path);
357847
+ if (parsedPath !== undefined && !parsedPath.ok)
357848
+ return parsedPath;
357849
+ const candidate = {
357850
+ type,
357851
+ ...flags.button === undefined ? {} : { button: flags.button },
357852
+ ...flags.keys === undefined ? {} : { keys: flags.keys },
357853
+ ...parsedPath === undefined ? {} : { path: parsedPath.value },
357854
+ ...flags.scrollX === undefined ? {} : { scroll_x: toNumber2(flags.scrollX) },
357855
+ ...flags.scrollY === undefined ? {} : { scroll_y: toNumber2(flags.scrollY) },
357856
+ ...flags.text === undefined ? {} : { text: flags.text },
357857
+ ...flags.url === undefined ? {} : { url: flags.url },
357858
+ ...flags.x === undefined ? {} : { x: toNumber2(flags.x) },
357859
+ ...flags.y === undefined ? {} : { y: toNumber2(flags.y) }
357860
+ };
357861
+ return parseBrowserAction(candidate);
357862
+ }
357863
+ function parseBrowserAction(candidate) {
357864
+ const parsed = browserActionSchema.safeParse(candidate);
357865
+ if (!parsed.success) {
357866
+ return { error: exports_external.prettifyError(parsed.error), ok: false };
357867
+ }
357868
+ return { action: parsed.data, ok: true };
357869
+ }
357870
+
357871
+ // src/domains/interactiveRunner/readAction.ts
357694
357872
  var stdinArgument2 = "-";
357695
357873
  async function readAction(type, flags, deps) {
357696
357874
  if (type !== stdinArgument2)
@@ -357708,6 +357886,8 @@ async function readAction(type, flags, deps) {
357708
357886
  return { error: interactiveRunnerMessages.actionNotJson, ok: false };
357709
357887
  }
357710
357888
  }
357889
+
357890
+ // src/domains/interactiveRunner/performAction.ts
357711
357891
  async function handleRunnerAct(ctx, options, deps) {
357712
357892
  const built = await readAction(options.type, options.flags, deps);
357713
357893
  if (!built.ok)
@@ -357821,7 +358001,7 @@ Examples:
357821
358001
  $ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
357822
358002
  function registerRunnerInteractCommands(runner, signals) {
357823
358003
  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, {
358004
+ 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
358005
  flags: {
357826
358006
  button: opts.button,
357827
358007
  keys: opts.keys,
@@ -358828,4 +359008,4 @@ createProgram({ signals }).parseAsync().catch(() => {
358828
359008
  process.exitCode = 1;
358829
359009
  }).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
358830
359010
 
358831
- //# debugId=B6D9FB052BE12D6D64756E2164756E21
359011
+ //# debugId=C46D821567854FC564756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/cli",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
5
5
  "keywords": [
6
6
  "automation",
@@ -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`