@yoqa/cli 0.3.28 → 0.3.30
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/main.js +542 -18
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -2122,7 +2122,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
2122
2122
|
});
|
|
2123
2123
|
|
|
2124
2124
|
// src/program.ts
|
|
2125
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
2125
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
|
|
2126
2126
|
|
|
2127
2127
|
// ../../node_modules/zod/v3/external.js
|
|
2128
2128
|
var exports_external = {};
|
|
@@ -6105,6 +6105,7 @@ var healthResponseSchema = exports_external.object({
|
|
|
6105
6105
|
uptimeMs: exports_external.number().nonnegative()
|
|
6106
6106
|
});
|
|
6107
6107
|
var devicePlatformSchema = exports_external.union([exports_external.literal("ios"), exports_external.literal("android")]);
|
|
6108
|
+
var laneNameSchema = exports_external.union([exports_external.literal("appium"), exports_external.literal("direct")]);
|
|
6108
6109
|
var deviceKindSchema = exports_external.union([
|
|
6109
6110
|
exports_external.literal("physical"),
|
|
6110
6111
|
exports_external.literal("simulator"),
|
|
@@ -6632,7 +6633,8 @@ var createRunRequestSchema = exports_external.object({
|
|
|
6632
6633
|
buildId: exports_external.string().min(1).optional(),
|
|
6633
6634
|
buildPath: exports_external.string().min(1).optional(),
|
|
6634
6635
|
executionMode: runExecutionModeSchema.optional(),
|
|
6635
|
-
screenMode: runScreenModeSchema.optional()
|
|
6636
|
+
screenMode: runScreenModeSchema.optional(),
|
|
6637
|
+
lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
|
|
6636
6638
|
});
|
|
6637
6639
|
var stepPhasesSchema = exports_external.object({
|
|
6638
6640
|
captureMs: exports_external.number().nonnegative(),
|
|
@@ -6677,6 +6679,8 @@ var runSchema = exports_external.object({
|
|
|
6677
6679
|
status: runStatusSchema,
|
|
6678
6680
|
executionMode: runExecutionModeSchema,
|
|
6679
6681
|
screenMode: runScreenModeSchema.optional(),
|
|
6682
|
+
lane: laneNameSchema.optional(),
|
|
6683
|
+
laneWarning: exports_external.string().min(1).optional(),
|
|
6680
6684
|
error: exports_external.string().nullable(),
|
|
6681
6685
|
createdAt: exports_external.number().int().nonnegative(),
|
|
6682
6686
|
startedAt: exports_external.number().int().nonnegative().nullable(),
|
|
@@ -6694,7 +6698,8 @@ var connectDeviceRequestSchema = exports_external.object({
|
|
|
6694
6698
|
deviceId: exports_external.string().min(1),
|
|
6695
6699
|
platform: devicePlatformSchema,
|
|
6696
6700
|
bundleId: exports_external.string().min(1).optional(),
|
|
6697
|
-
appPackage: exports_external.string().min(1).optional()
|
|
6701
|
+
appPackage: exports_external.string().min(1).optional(),
|
|
6702
|
+
lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
|
|
6698
6703
|
});
|
|
6699
6704
|
var activeDeviceResponseSchema = exports_external.object({
|
|
6700
6705
|
deviceId: exports_external.string().min(1),
|
|
@@ -6703,7 +6708,9 @@ var activeDeviceResponseSchema = exports_external.object({
|
|
|
6703
6708
|
mjpegPort: exports_external.number().int().positive().optional(),
|
|
6704
6709
|
streamReady: exports_external.boolean().optional(),
|
|
6705
6710
|
streamUrl: exports_external.string().min(1).optional(),
|
|
6706
|
-
heldByRun: exports_external.boolean().optional()
|
|
6711
|
+
heldByRun: exports_external.boolean().optional(),
|
|
6712
|
+
lane: laneNameSchema.optional(),
|
|
6713
|
+
laneWarning: exports_external.string().min(1).optional()
|
|
6707
6714
|
});
|
|
6708
6715
|
var controlPointerMessageSchema = exports_external.object({
|
|
6709
6716
|
type: exports_external.literal("pointer"),
|
|
@@ -6764,15 +6771,23 @@ var actionRequestSchema = exports_external.object({
|
|
|
6764
6771
|
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
|
|
6765
6772
|
seconds: exports_external.number().optional(),
|
|
6766
6773
|
screenshot: exports_external.boolean().optional(),
|
|
6767
|
-
settleMs: exports_external.number().int().min(0).max(1e4).optional()
|
|
6774
|
+
settleMs: exports_external.number().int().min(0).max(1e4).optional(),
|
|
6775
|
+
fullImage: exports_external.boolean().optional(),
|
|
6776
|
+
imageScale: exports_external.number().min(0.01).max(1).optional()
|
|
6768
6777
|
});
|
|
6769
6778
|
var actionResultScreenshotSchema = exports_external.object({
|
|
6770
6779
|
path: exports_external.string().min(1),
|
|
6780
|
+
rawPath: exports_external.string().min(1).optional(),
|
|
6771
6781
|
annotatedPath: exports_external.string().min(1).optional(),
|
|
6772
6782
|
settled: exports_external.boolean(),
|
|
6773
6783
|
waitedMs: exports_external.number(),
|
|
6774
6784
|
changed: exports_external.boolean().nullable()
|
|
6775
6785
|
});
|
|
6786
|
+
var actionPhasesSchema = exports_external.object({
|
|
6787
|
+
captureMs: exports_external.number().nonnegative(),
|
|
6788
|
+
actionMs: exports_external.number().nonnegative(),
|
|
6789
|
+
settleMs: exports_external.number().nonnegative()
|
|
6790
|
+
});
|
|
6776
6791
|
var actionResponseSchema = exports_external.object({
|
|
6777
6792
|
ok: exports_external.literal(true),
|
|
6778
6793
|
kind: actionKindSchema,
|
|
@@ -6780,7 +6795,23 @@ var actionResponseSchema = exports_external.object({
|
|
|
6780
6795
|
x: exports_external.number().optional(),
|
|
6781
6796
|
y: exports_external.number().optional()
|
|
6782
6797
|
}).optional(),
|
|
6783
|
-
screenshot: actionResultScreenshotSchema.optional()
|
|
6798
|
+
screenshot: actionResultScreenshotSchema.optional(),
|
|
6799
|
+
phases: actionPhasesSchema.optional()
|
|
6800
|
+
});
|
|
6801
|
+
var actionBatchRequestSchema = exports_external.object({
|
|
6802
|
+
steps: exports_external.array(actionRequestSchema).min(1),
|
|
6803
|
+
settleMs: exports_external.number().int().min(0).max(1e4).optional(),
|
|
6804
|
+
fullImage: exports_external.boolean().optional(),
|
|
6805
|
+
imageScale: exports_external.number().min(0.01).max(1).optional()
|
|
6806
|
+
});
|
|
6807
|
+
var actionBatchResponseSchema = exports_external.object({
|
|
6808
|
+
ok: exports_external.boolean(),
|
|
6809
|
+
completed: exports_external.number().int().nonnegative(),
|
|
6810
|
+
total: exports_external.number().int().nonnegative(),
|
|
6811
|
+
failedIndex: exports_external.number().int().nonnegative().optional(),
|
|
6812
|
+
error: exports_external.string().optional(),
|
|
6813
|
+
screenshot: actionResultScreenshotSchema.optional(),
|
|
6814
|
+
steps: exports_external.array(actionResponseSchema)
|
|
6784
6815
|
});
|
|
6785
6816
|
var yoqaStatusResponseSchema = exports_external.object({
|
|
6786
6817
|
runner: exports_external.object({
|
|
@@ -7600,6 +7631,8 @@ function buildRunReportFromCatalogRun(run, meta = {}, screenshotsByStepId = {})
|
|
|
7600
7631
|
platform: run.platform,
|
|
7601
7632
|
executionMode: run.executionMode,
|
|
7602
7633
|
screenMode: run.screenMode ?? null,
|
|
7634
|
+
lane: run.lane ?? null,
|
|
7635
|
+
laneWarning: run.laneWarning ?? null,
|
|
7603
7636
|
error: run.error,
|
|
7604
7637
|
createdAt: run.createdAt,
|
|
7605
7638
|
startedAt: run.startedAt,
|
|
@@ -7647,7 +7680,11 @@ function pngDataUri(base64) {
|
|
|
7647
7680
|
}
|
|
7648
7681
|
function formatRunReportHtml(doc) {
|
|
7649
7682
|
const accent = statusAccent(doc.status);
|
|
7650
|
-
const screenRows =
|
|
7683
|
+
const screenRows = [
|
|
7684
|
+
...doc.screenMode ? [["Screen", doc.screenMode]] : [],
|
|
7685
|
+
...doc.lane ? [["Lane", doc.lane]] : [],
|
|
7686
|
+
...doc.laneWarning ? [["Lane warning", doc.laneWarning]] : []
|
|
7687
|
+
];
|
|
7651
7688
|
const metaRows = [
|
|
7652
7689
|
["Status", accent.label],
|
|
7653
7690
|
["Source", doc.source === "catalog" ? "Catalog run" : "Manual Inspector"],
|
|
@@ -7782,6 +7819,8 @@ function formatRunReportMarkdown(doc) {
|
|
|
7782
7819
|
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
7783
7820
|
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
7784
7821
|
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
7822
|
+
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
7823
|
+
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
7785
7824
|
`| Started | ${escapeMd(formatWhen(doc.startedAt ?? doc.createdAt))} |`,
|
|
7786
7825
|
`| Finished | ${escapeMd(formatWhen(doc.finishedAt))} |`,
|
|
7787
7826
|
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
@@ -7849,6 +7888,8 @@ function formatRunReportGithubSummary(doc) {
|
|
|
7849
7888
|
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
7850
7889
|
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
7851
7890
|
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
7891
|
+
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
7892
|
+
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
7852
7893
|
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
7853
7894
|
`| Run ID | \`${doc.id}\` |`,
|
|
7854
7895
|
""
|
|
@@ -8304,6 +8345,15 @@ class RunnerClient {
|
|
|
8304
8345
|
}, "Action failed");
|
|
8305
8346
|
return actionResponseSchema.parse(json);
|
|
8306
8347
|
}
|
|
8348
|
+
async performActionBatch(request) {
|
|
8349
|
+
const body = actionBatchRequestSchema.parse(request);
|
|
8350
|
+
const json = await this.requestJson("/action/batch", {
|
|
8351
|
+
method: "POST",
|
|
8352
|
+
headers: { "Content-Type": "application/json" },
|
|
8353
|
+
body: JSON.stringify(body)
|
|
8354
|
+
}, "Action batch failed");
|
|
8355
|
+
return actionBatchResponseSchema.parse(json);
|
|
8356
|
+
}
|
|
8307
8357
|
async getStatus() {
|
|
8308
8358
|
const json = await this.requestJson("/status", undefined, "Status failed");
|
|
8309
8359
|
return yoqaStatusResponseSchema.parse(json);
|
|
@@ -8352,10 +8402,369 @@ var {
|
|
|
8352
8402
|
Option,
|
|
8353
8403
|
Help
|
|
8354
8404
|
} = import__.default;
|
|
8405
|
+
|
|
8406
|
+
// ../../services/runner/src/domains/benchmark/accuracy.ts
|
|
8407
|
+
function pointInElement(element, x, y) {
|
|
8408
|
+
return x >= element.x && x <= element.x + element.width && y >= element.y && y <= element.y + element.height;
|
|
8409
|
+
}
|
|
8410
|
+
function tapHitsElement(elements, x, y, expect) {
|
|
8411
|
+
const intended = expect?.id ? elements.filter((el) => el.id === expect.id) : expect?.label ? elements.filter((el) => el.label === expect.label) : elements;
|
|
8412
|
+
return intended.some((el) => pointInElement(el, x, y));
|
|
8413
|
+
}
|
|
8414
|
+
|
|
8415
|
+
// ../../services/runner/src/domains/benchmark/drivers.ts
|
|
8416
|
+
function yoqaBenchmarkDriver(client, options) {
|
|
8417
|
+
return {
|
|
8418
|
+
name: "yoqa",
|
|
8419
|
+
connect: async () => {
|
|
8420
|
+
await client.connectDevice({
|
|
8421
|
+
deviceId: options.deviceId,
|
|
8422
|
+
platform: options.platform,
|
|
8423
|
+
lane: options.lane
|
|
8424
|
+
});
|
|
8425
|
+
},
|
|
8426
|
+
screen: async () => {
|
|
8427
|
+
await client.getScreen();
|
|
8428
|
+
},
|
|
8429
|
+
tapToResult: async (x, y, expect) => {
|
|
8430
|
+
const result = await client.performAction({ kind: "tap", x, y, screenshot: true });
|
|
8431
|
+
const tree = await client.getScreen().catch(() => ({ elements: [] }));
|
|
8432
|
+
const hit = tapHitsElement(tree.elements ?? [], x, y, expect);
|
|
8433
|
+
return {
|
|
8434
|
+
hit,
|
|
8435
|
+
phases: {
|
|
8436
|
+
capture: result.phases?.captureMs ?? 0,
|
|
8437
|
+
action: result.phases?.actionMs ?? 0,
|
|
8438
|
+
settle: result.phases?.settleMs ?? result.screenshot?.waitedMs ?? 0
|
|
8439
|
+
}
|
|
8440
|
+
};
|
|
8441
|
+
},
|
|
8442
|
+
runCase: client.createRun && client.waitForRun && options.appId ? async (caseId) => {
|
|
8443
|
+
const created = await client.createRun?.({
|
|
8444
|
+
appId: options.appId ?? "",
|
|
8445
|
+
caseIds: [caseId],
|
|
8446
|
+
deviceId: options.deviceId,
|
|
8447
|
+
platform: options.platform,
|
|
8448
|
+
executionMode: "agent",
|
|
8449
|
+
screenMode: options.screenMode,
|
|
8450
|
+
lane: options.lane
|
|
8451
|
+
});
|
|
8452
|
+
if (!created)
|
|
8453
|
+
return { passed: false };
|
|
8454
|
+
const finished = await client.waitForRun?.(created.id);
|
|
8455
|
+
const steps = finished?.tests?.reduce((n, test) => n + (test.steps?.length ?? 0), 0);
|
|
8456
|
+
return { passed: finished?.status === "passed", steps };
|
|
8457
|
+
} : undefined,
|
|
8458
|
+
disconnect: async () => {
|
|
8459
|
+
await client.disconnectDevice();
|
|
8460
|
+
}
|
|
8461
|
+
};
|
|
8462
|
+
}
|
|
8463
|
+
|
|
8464
|
+
// ../../services/runner/src/domains/benchmark/scenario.ts
|
|
8465
|
+
async function runLatencyScenario(driver, options) {
|
|
8466
|
+
const clock = options.clock ?? { now: () => Date.now() };
|
|
8467
|
+
const taps = options.suite?.taps ?? [{ x: options.x ?? 500, y: options.y ?? 500 }];
|
|
8468
|
+
const tapToResult = [];
|
|
8469
|
+
const screenRead = [];
|
|
8470
|
+
const coldStart = [];
|
|
8471
|
+
const tapHits = [];
|
|
8472
|
+
const casePasses = [];
|
|
8473
|
+
const stepCounts = [];
|
|
8474
|
+
const captures = [];
|
|
8475
|
+
const actions = [];
|
|
8476
|
+
const settles = [];
|
|
8477
|
+
const t0 = clock.now();
|
|
8478
|
+
await driver.connect();
|
|
8479
|
+
options.advance?.(50);
|
|
8480
|
+
coldStart.push(clock.now() - t0);
|
|
8481
|
+
const tScreen = clock.now();
|
|
8482
|
+
await driver.screen();
|
|
8483
|
+
options.advance?.(20);
|
|
8484
|
+
screenRead.push(clock.now() - tScreen);
|
|
8485
|
+
for (let i = 0;i < options.repeats; i++) {
|
|
8486
|
+
const tap = taps[i % taps.length];
|
|
8487
|
+
if (!tap)
|
|
8488
|
+
continue;
|
|
8489
|
+
const tTap = clock.now();
|
|
8490
|
+
const result = await driver.tapToResult(tap.x, tap.y, {
|
|
8491
|
+
id: tap.id,
|
|
8492
|
+
label: tap.label
|
|
8493
|
+
});
|
|
8494
|
+
options.advance?.(30);
|
|
8495
|
+
tapToResult.push(clock.now() - tTap);
|
|
8496
|
+
if (result.hit != null)
|
|
8497
|
+
tapHits.push(result.hit);
|
|
8498
|
+
if (result.phases?.capture != null)
|
|
8499
|
+
captures.push(result.phases.capture);
|
|
8500
|
+
if (result.phases?.action != null)
|
|
8501
|
+
actions.push(result.phases.action);
|
|
8502
|
+
if (result.phases?.settle != null)
|
|
8503
|
+
settles.push(result.phases.settle);
|
|
8504
|
+
}
|
|
8505
|
+
if (driver.runCase && options.suite) {
|
|
8506
|
+
const times = options.suite.passRepeats;
|
|
8507
|
+
for (let i = 0;i < times; i++) {
|
|
8508
|
+
for (const item of options.suite.cases) {
|
|
8509
|
+
const outcome = await driver.runCase(item.caseId);
|
|
8510
|
+
const passed = typeof outcome === "boolean" ? outcome : outcome.passed;
|
|
8511
|
+
casePasses.push(passed);
|
|
8512
|
+
if (typeof outcome !== "boolean" && outcome.steps != null) {
|
|
8513
|
+
stepCounts.push(outcome.steps);
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
await driver.disconnect();
|
|
8519
|
+
const phases = driver.name === "yoqa" && (captures.length || actions.length || settles.length) ? {
|
|
8520
|
+
...captures.length ? { capture: captures } : {},
|
|
8521
|
+
...actions.length ? { action: actions } : {},
|
|
8522
|
+
...settles.length ? { settle: settles } : {}
|
|
8523
|
+
} : undefined;
|
|
8524
|
+
return {
|
|
8525
|
+
tool: driver.name,
|
|
8526
|
+
platform: options.platform ?? "ios",
|
|
8527
|
+
...options.screenMode ? { screenMode: options.screenMode } : {},
|
|
8528
|
+
tapToResult,
|
|
8529
|
+
screenRead,
|
|
8530
|
+
coldStart,
|
|
8531
|
+
...tapHits.length ? { tapHits } : {},
|
|
8532
|
+
...casePasses.length ? { casePasses } : {},
|
|
8533
|
+
...stepCounts.length ? { stepCounts } : {},
|
|
8534
|
+
...phases ? { phases } : {}
|
|
8535
|
+
};
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8538
|
+
// ../../services/runner/src/domains/benchmark/suite.ts
|
|
8539
|
+
var BENCHMARK_SUITE_VERSION = 1;
|
|
8540
|
+
var DEFAULT_BENCHMARK_SUITE = {
|
|
8541
|
+
version: BENCHMARK_SUITE_VERSION,
|
|
8542
|
+
repeats: 5,
|
|
8543
|
+
taps: [{ x: 500, y: 500 }],
|
|
8544
|
+
cases: [],
|
|
8545
|
+
passRepeats: 3,
|
|
8546
|
+
arms: ["vision"]
|
|
8547
|
+
};
|
|
8548
|
+
function parseBenchmarkSuite(raw) {
|
|
8549
|
+
if (!raw || typeof raw !== "object") {
|
|
8550
|
+
throw new Error("Benchmark suite must be an object");
|
|
8551
|
+
}
|
|
8552
|
+
const value = raw;
|
|
8553
|
+
const version = Number(value.version);
|
|
8554
|
+
const repeats = Number(value.repeats);
|
|
8555
|
+
const passRepeats = Number(value.passRepeats ?? 3);
|
|
8556
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
8557
|
+
throw new Error("Benchmark suite version must be a positive integer");
|
|
8558
|
+
}
|
|
8559
|
+
if (!Number.isInteger(repeats) || repeats < 1) {
|
|
8560
|
+
throw new Error("Benchmark suite repeats must be a positive integer");
|
|
8561
|
+
}
|
|
8562
|
+
if (!Array.isArray(value.taps) || value.taps.length === 0) {
|
|
8563
|
+
throw new Error("Benchmark suite needs at least one tap");
|
|
8564
|
+
}
|
|
8565
|
+
const taps = value.taps.map((tap, i) => {
|
|
8566
|
+
if (!tap || typeof tap !== "object") {
|
|
8567
|
+
throw new Error(`Benchmark suite tap ${i} is invalid`);
|
|
8568
|
+
}
|
|
8569
|
+
const row = tap;
|
|
8570
|
+
const x = Number(row.x);
|
|
8571
|
+
const y = Number(row.y);
|
|
8572
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
8573
|
+
throw new Error(`Benchmark suite tap ${i} needs x and y`);
|
|
8574
|
+
}
|
|
8575
|
+
return {
|
|
8576
|
+
x,
|
|
8577
|
+
y,
|
|
8578
|
+
...typeof row.id === "string" ? { id: row.id } : {},
|
|
8579
|
+
...typeof row.label === "string" ? { label: row.label } : {}
|
|
8580
|
+
};
|
|
8581
|
+
});
|
|
8582
|
+
const cases = Array.isArray(value.cases) ? value.cases.flatMap((item) => {
|
|
8583
|
+
if (!item || typeof item !== "object")
|
|
8584
|
+
return [];
|
|
8585
|
+
const caseId = item.caseId;
|
|
8586
|
+
return typeof caseId === "string" && caseId.length > 0 ? [{ caseId }] : [];
|
|
8587
|
+
}) : [];
|
|
8588
|
+
return {
|
|
8589
|
+
version,
|
|
8590
|
+
repeats,
|
|
8591
|
+
taps,
|
|
8592
|
+
cases,
|
|
8593
|
+
passRepeats: Number.isInteger(passRepeats) ? passRepeats : 3,
|
|
8594
|
+
arms: parseBenchmarkArms(value.arms)
|
|
8595
|
+
};
|
|
8596
|
+
}
|
|
8597
|
+
function parseBenchmarkArms(raw) {
|
|
8598
|
+
if (raw == null)
|
|
8599
|
+
return ["vision"];
|
|
8600
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
8601
|
+
throw new Error("Benchmark suite arms must be a non-empty list of vision | tree");
|
|
8602
|
+
}
|
|
8603
|
+
const arms = [];
|
|
8604
|
+
for (const item of raw) {
|
|
8605
|
+
if (item !== "vision" && item !== "tree") {
|
|
8606
|
+
throw new Error("Benchmark suite arms must be vision or tree");
|
|
8607
|
+
}
|
|
8608
|
+
if (!arms.includes(item))
|
|
8609
|
+
arms.push(item);
|
|
8610
|
+
}
|
|
8611
|
+
return arms;
|
|
8612
|
+
}
|
|
8613
|
+
|
|
8614
|
+
// ../../services/runner/src/domains/benchmark/summarize.ts
|
|
8615
|
+
function percentile(samples, p) {
|
|
8616
|
+
if (samples.length === 0)
|
|
8617
|
+
return 0;
|
|
8618
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
8619
|
+
const rank = Math.ceil(p / 100 * sorted.length) - 1;
|
|
8620
|
+
return sorted[Math.max(0, Math.min(sorted.length - 1, rank))] ?? 0;
|
|
8621
|
+
}
|
|
8622
|
+
function summarizeSamples(samples) {
|
|
8623
|
+
return {
|
|
8624
|
+
p50: percentile(samples, 50),
|
|
8625
|
+
p95: percentile(samples, 95),
|
|
8626
|
+
n: samples.length,
|
|
8627
|
+
samples: [...samples]
|
|
8628
|
+
};
|
|
8629
|
+
}
|
|
8630
|
+
function mean(samples) {
|
|
8631
|
+
if (!samples || samples.length === 0)
|
|
8632
|
+
return;
|
|
8633
|
+
return samples.reduce((sum, n) => sum + n, 0) / samples.length;
|
|
8634
|
+
}
|
|
8635
|
+
function summarizeRate(hits) {
|
|
8636
|
+
if (!hits || hits.length === 0)
|
|
8637
|
+
return;
|
|
8638
|
+
const n = hits.length;
|
|
8639
|
+
const count = hits.filter(Boolean).length;
|
|
8640
|
+
return { hits: count, n, rate: count / n };
|
|
8641
|
+
}
|
|
8642
|
+
function summarizeLatency(input) {
|
|
8643
|
+
return {
|
|
8644
|
+
recordedAt: input.recordedAt,
|
|
8645
|
+
repeats: input.repeats,
|
|
8646
|
+
...input.suiteVersion != null ? { suiteVersion: input.suiteVersion } : {},
|
|
8647
|
+
results: input.runs.map((run) => {
|
|
8648
|
+
const capture = mean(run.phases?.capture);
|
|
8649
|
+
const action = mean(run.phases?.action);
|
|
8650
|
+
const settle = mean(run.phases?.settle);
|
|
8651
|
+
const phases = capture != null || action != null || settle != null ? {
|
|
8652
|
+
...capture != null ? { capture } : {},
|
|
8653
|
+
...action != null ? { action } : {},
|
|
8654
|
+
...settle != null ? { settle } : {}
|
|
8655
|
+
} : undefined;
|
|
8656
|
+
const accuracy = summarizeRate(run.tapHits);
|
|
8657
|
+
const passRate = summarizeRate(run.casePasses);
|
|
8658
|
+
const steps = run.stepCounts?.length ? summarizeSamples(run.stepCounts) : undefined;
|
|
8659
|
+
return {
|
|
8660
|
+
tool: run.tool,
|
|
8661
|
+
platform: run.platform,
|
|
8662
|
+
...run.screenMode ? { screenMode: run.screenMode } : {},
|
|
8663
|
+
metrics: {
|
|
8664
|
+
tapToResult: summarizeSamples(run.tapToResult),
|
|
8665
|
+
screenRead: summarizeSamples(run.screenRead),
|
|
8666
|
+
coldStart: summarizeSamples(run.coldStart)
|
|
8667
|
+
},
|
|
8668
|
+
...accuracy ? { accuracy } : {},
|
|
8669
|
+
...passRate ? { passRate } : {},
|
|
8670
|
+
...steps ? { steps } : {},
|
|
8671
|
+
...phases ? { phases } : {}
|
|
8672
|
+
};
|
|
8673
|
+
})
|
|
8674
|
+
};
|
|
8675
|
+
}
|
|
8676
|
+
function formatLatencyTable(report) {
|
|
8677
|
+
const header = [
|
|
8678
|
+
"tool".padEnd(12),
|
|
8679
|
+
"platform".padEnd(10),
|
|
8680
|
+
"metric".padEnd(14),
|
|
8681
|
+
"p50".padStart(8),
|
|
8682
|
+
"p95".padStart(8),
|
|
8683
|
+
"n".padStart(4)
|
|
8684
|
+
].join(" ");
|
|
8685
|
+
const lines = [header];
|
|
8686
|
+
for (const row of report.results) {
|
|
8687
|
+
const tool = row.screenMode ? `${row.tool}/${row.screenMode}` : row.tool;
|
|
8688
|
+
for (const metric of ["tapToResult", "screenRead", "coldStart"]) {
|
|
8689
|
+
const s = row.metrics[metric];
|
|
8690
|
+
lines.push([
|
|
8691
|
+
tool.padEnd(12),
|
|
8692
|
+
row.platform.padEnd(10),
|
|
8693
|
+
metric.padEnd(14),
|
|
8694
|
+
String(Math.round(s.p50)).padStart(8),
|
|
8695
|
+
String(Math.round(s.p95)).padStart(8),
|
|
8696
|
+
String(s.n).padStart(4)
|
|
8697
|
+
].join(" "));
|
|
8698
|
+
}
|
|
8699
|
+
if (row.accuracy) {
|
|
8700
|
+
lines.push([
|
|
8701
|
+
tool.padEnd(12),
|
|
8702
|
+
row.platform.padEnd(10),
|
|
8703
|
+
"tapAccuracy".padEnd(14),
|
|
8704
|
+
row.accuracy.rate.toFixed(2).padStart(8),
|
|
8705
|
+
"".padStart(8),
|
|
8706
|
+
String(row.accuracy.n).padStart(4)
|
|
8707
|
+
].join(" "));
|
|
8708
|
+
}
|
|
8709
|
+
if (row.passRate) {
|
|
8710
|
+
lines.push([
|
|
8711
|
+
tool.padEnd(12),
|
|
8712
|
+
row.platform.padEnd(10),
|
|
8713
|
+
"casePassRate".padEnd(14),
|
|
8714
|
+
row.passRate.rate.toFixed(2).padStart(8),
|
|
8715
|
+
"".padStart(8),
|
|
8716
|
+
String(row.passRate.n).padStart(4)
|
|
8717
|
+
].join(" "));
|
|
8718
|
+
}
|
|
8719
|
+
if (row.steps) {
|
|
8720
|
+
lines.push([
|
|
8721
|
+
tool.padEnd(12),
|
|
8722
|
+
row.platform.padEnd(10),
|
|
8723
|
+
"stepCount".padEnd(14),
|
|
8724
|
+
String(Math.round(row.steps.p50)).padStart(8),
|
|
8725
|
+
String(Math.round(row.steps.p95)).padStart(8),
|
|
8726
|
+
String(row.steps.n).padStart(4)
|
|
8727
|
+
].join(" "));
|
|
8728
|
+
}
|
|
8729
|
+
if (row.phases) {
|
|
8730
|
+
const parts = Object.entries(row.phases).map(([k, v]) => `${k}=${Math.round(v)}`);
|
|
8731
|
+
lines.push(` phases ${parts.join(" ")}`);
|
|
8732
|
+
}
|
|
8733
|
+
}
|
|
8734
|
+
return `${lines.join(`
|
|
8735
|
+
`)}
|
|
8736
|
+
`;
|
|
8737
|
+
}
|
|
8738
|
+
function recommendScreenDefault(vision, tree) {
|
|
8739
|
+
if (!vision || !tree) {
|
|
8740
|
+
return {
|
|
8741
|
+
screenMode: "vision",
|
|
8742
|
+
reason: "Need both vision and tree arms before flipping the default"
|
|
8743
|
+
};
|
|
8744
|
+
}
|
|
8745
|
+
if (!vision.passRate || !tree.passRate) {
|
|
8746
|
+
return {
|
|
8747
|
+
screenMode: "vision",
|
|
8748
|
+
reason: "No Case pass-rate samples; keep vision-first"
|
|
8749
|
+
};
|
|
8750
|
+
}
|
|
8751
|
+
const visionAcc = vision.accuracy?.rate ?? 0;
|
|
8752
|
+
const treeAcc = tree.accuracy?.rate ?? 0;
|
|
8753
|
+
if (tree.passRate.rate > vision.passRate.rate && treeAcc >= visionAcc) {
|
|
8754
|
+
return {
|
|
8755
|
+
screenMode: "tree",
|
|
8756
|
+
reason: "Tree arm won Case pass rate without losing tap accuracy"
|
|
8757
|
+
};
|
|
8758
|
+
}
|
|
8759
|
+
return {
|
|
8760
|
+
screenMode: "vision",
|
|
8761
|
+
reason: "Tree arm did not beat vision on pass rate and accuracy; keep vision-first"
|
|
8762
|
+
};
|
|
8763
|
+
}
|
|
8355
8764
|
// package.json
|
|
8356
8765
|
var package_default = {
|
|
8357
8766
|
name: "@yoqa/cli",
|
|
8358
|
-
version: "0.3.
|
|
8767
|
+
version: "0.3.30",
|
|
8359
8768
|
description: "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
|
|
8360
8769
|
license: "UNLICENSED",
|
|
8361
8770
|
type: "module",
|
|
@@ -8393,7 +8802,7 @@ var package_default = {
|
|
|
8393
8802
|
health: "bun run src/main.ts health"
|
|
8394
8803
|
},
|
|
8395
8804
|
dependencies: {
|
|
8396
|
-
"@yoqa/runner": "0.3.
|
|
8805
|
+
"@yoqa/runner": "0.3.30"
|
|
8397
8806
|
},
|
|
8398
8807
|
devDependencies: {
|
|
8399
8808
|
"@types/node": "^22.15.30",
|
|
@@ -9244,23 +9653,30 @@ for (const platform of ["ios", "android"]) {
|
|
|
9244
9653
|
}
|
|
9245
9654
|
});
|
|
9246
9655
|
}
|
|
9247
|
-
devices.command("connect").description("Open
|
|
9656
|
+
devices.command("connect").description("Open a Device Session on a device").argument("<deviceId>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--bundle-id <id>", "iOS bundle id to launch").option("--app-package <id>", "Android application id to launch").option("--lane <lane>", "appium | direct | auto (Android / iOS sim auto → Direct)").option("--json", "Print raw JSON").action(async (deviceId, options) => {
|
|
9248
9657
|
try {
|
|
9249
9658
|
const platform = options.platform;
|
|
9250
9659
|
if (platform !== "ios" && platform !== "android") {
|
|
9251
9660
|
throw new Error("--platform must be ios or android");
|
|
9252
9661
|
}
|
|
9662
|
+
const lane = options.lane;
|
|
9663
|
+
if (lane != null && lane !== "appium" && lane !== "direct" && lane !== "auto") {
|
|
9664
|
+
throw new Error("--lane must be appium, direct, or auto");
|
|
9665
|
+
}
|
|
9253
9666
|
const body = await client(options.baseUrl).connectDevice({
|
|
9254
9667
|
deviceId,
|
|
9255
9668
|
platform,
|
|
9256
9669
|
bundleId: options.bundleId,
|
|
9257
|
-
appPackage: options.appPackage
|
|
9670
|
+
appPackage: options.appPackage,
|
|
9671
|
+
lane
|
|
9258
9672
|
});
|
|
9259
9673
|
if (options.json) {
|
|
9260
9674
|
console.log(JSON.stringify(body, null, 2));
|
|
9261
9675
|
return;
|
|
9262
9676
|
}
|
|
9263
|
-
console.log(`connected ${body.platform} ${body.deviceId}`);
|
|
9677
|
+
console.log(`connected ${body.platform} ${body.deviceId} lane ${body.lane ?? "appium"}`);
|
|
9678
|
+
if (body.laneWarning)
|
|
9679
|
+
console.log(`lane warning: ${body.laneWarning}`);
|
|
9264
9680
|
} catch (error) {
|
|
9265
9681
|
fail("devices connect", error);
|
|
9266
9682
|
}
|
|
@@ -9277,7 +9693,9 @@ devices.command("active").description("Show the currently connected device").opt
|
|
|
9277
9693
|
console.log(JSON.stringify(body, null, 2));
|
|
9278
9694
|
return;
|
|
9279
9695
|
}
|
|
9280
|
-
console.log(`${body.platform} ${body.deviceId}`);
|
|
9696
|
+
console.log(`${body.platform} ${body.deviceId} lane ${body.lane ?? "appium"}`);
|
|
9697
|
+
if (body.laneWarning)
|
|
9698
|
+
console.log(`lane warning: ${body.laneWarning}`);
|
|
9281
9699
|
} catch (error) {
|
|
9282
9700
|
fail("devices active", error);
|
|
9283
9701
|
}
|
|
@@ -9322,15 +9740,20 @@ program2.command("screenshot").description("Capture a screenshot from the active
|
|
|
9322
9740
|
});
|
|
9323
9741
|
var action = program2.command("action").description("Perform an action on the active device");
|
|
9324
9742
|
function addActionOptions(cmd) {
|
|
9325
|
-
return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v)).option("--screenshot", "Wait for the screen to settle, then return the result screenshot (and a copy marking where the tap or swipe landed)").option("--settle <ms>", "With --screenshot: longest wait for the screen to settle (default 1500)", (v) => Number(v));
|
|
9743
|
+
return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v)).option("--screenshot", "Wait for the screen to settle, then return the result screenshot (and a copy marking where the tap or swipe landed)").option("--settle <ms>", "With --screenshot: longest wait for the screen to settle (default 1500)", (v) => Number(v)).option("--full", "With --screenshot: return the raw Result screenshot instead of the Agent image").option("--scale <n>", "With --screenshot: Agent image scale 0.01–1 (default: long edge ~1000 px)", (v) => Number(v));
|
|
9326
9744
|
}
|
|
9327
9745
|
function printResultScreenshot(shot) {
|
|
9328
9746
|
console.log(`screenshot ${shot.path}`);
|
|
9747
|
+
if (shot.rawPath)
|
|
9748
|
+
console.log(`raw ${shot.rawPath}`);
|
|
9329
9749
|
if (shot.annotatedPath)
|
|
9330
9750
|
console.log(`marked ${shot.annotatedPath}`);
|
|
9331
9751
|
console.log(`settled ${shot.settled} (${Math.round(shot.waitedMs)}ms)`);
|
|
9332
9752
|
console.log(`changed ${shot.changed === null ? "unknown (screen still animating)" : shot.changed}`);
|
|
9333
9753
|
}
|
|
9754
|
+
function printActionPhases(phases) {
|
|
9755
|
+
console.log(`phases capture=${phases.captureMs} action=${phases.actionMs} settle=${phases.settleMs}`);
|
|
9756
|
+
}
|
|
9334
9757
|
for (const kind of [
|
|
9335
9758
|
"tap",
|
|
9336
9759
|
"swipe",
|
|
@@ -9360,7 +9783,9 @@ for (const kind of [
|
|
|
9360
9783
|
url: options.url,
|
|
9361
9784
|
seconds: options.seconds,
|
|
9362
9785
|
screenshot: options.screenshot === true ? true : undefined,
|
|
9363
|
-
settleMs: options.settle
|
|
9786
|
+
settleMs: options.settle,
|
|
9787
|
+
fullImage: options.full === true ? true : undefined,
|
|
9788
|
+
imageScale: options.scale
|
|
9364
9789
|
});
|
|
9365
9790
|
if (options.json) {
|
|
9366
9791
|
console.log(JSON.stringify(body, null, 2));
|
|
@@ -9372,6 +9797,8 @@ for (const kind of [
|
|
|
9372
9797
|
}
|
|
9373
9798
|
if (body.screenshot)
|
|
9374
9799
|
printResultScreenshot(body.screenshot);
|
|
9800
|
+
if (body.phases)
|
|
9801
|
+
printActionPhases(body.phases);
|
|
9375
9802
|
} catch (error) {
|
|
9376
9803
|
fail(`action ${kind}`, error);
|
|
9377
9804
|
}
|
|
@@ -9383,7 +9810,9 @@ addActionOptions(action.command("alert").description("Accept or dismiss a system
|
|
|
9383
9810
|
kind: "alert",
|
|
9384
9811
|
alertAction: options.dismiss ? "dismiss" : "accept",
|
|
9385
9812
|
screenshot: options.screenshot === true ? true : undefined,
|
|
9386
|
-
settleMs: options.settle
|
|
9813
|
+
settleMs: options.settle,
|
|
9814
|
+
fullImage: options.full === true ? true : undefined,
|
|
9815
|
+
imageScale: options.scale
|
|
9387
9816
|
});
|
|
9388
9817
|
if (options.json) {
|
|
9389
9818
|
console.log(JSON.stringify(body, null, 2));
|
|
@@ -9396,6 +9825,44 @@ addActionOptions(action.command("alert").description("Accept or dismiss a system
|
|
|
9396
9825
|
fail("action alert", error);
|
|
9397
9826
|
}
|
|
9398
9827
|
});
|
|
9828
|
+
addActionOptions(action.command("batch").description("Run known Actions from a JSON file or stdin (one Settle and Result screenshot at the end). Description-grounded steps are rejected.").argument("[file]", "JSON file of { steps: Action[] } or an Action array. Omit to read stdin.")).action(async (file, options) => {
|
|
9829
|
+
try {
|
|
9830
|
+
const raw = file ? await readFile2(file, "utf8") : await new Promise((resolve, reject) => {
|
|
9831
|
+
const chunks = [];
|
|
9832
|
+
process.stdin.on("data", (chunk) => {
|
|
9833
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
9834
|
+
});
|
|
9835
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
9836
|
+
process.stdin.on("error", reject);
|
|
9837
|
+
});
|
|
9838
|
+
const parsed = JSON.parse(raw);
|
|
9839
|
+
const steps = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && ("steps" in parsed) ? parsed.steps : null;
|
|
9840
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
9841
|
+
throw new Error("Batch JSON must be an Action array or { steps: Action[] }");
|
|
9842
|
+
}
|
|
9843
|
+
const body = await client(String(options.baseUrl)).performActionBatch({
|
|
9844
|
+
steps,
|
|
9845
|
+
settleMs: options.settle,
|
|
9846
|
+
fullImage: options.full === true ? true : undefined,
|
|
9847
|
+
imageScale: options.scale
|
|
9848
|
+
});
|
|
9849
|
+
if (options.json) {
|
|
9850
|
+
console.log(JSON.stringify(body, null, 2));
|
|
9851
|
+
return;
|
|
9852
|
+
}
|
|
9853
|
+
console.log(`batch ${body.ok ? "ok" : "failed"} ${body.completed}/${body.total}`);
|
|
9854
|
+
if (body.failedIndex != null)
|
|
9855
|
+
console.log(`failedIndex ${body.failedIndex}`);
|
|
9856
|
+
if (body.error)
|
|
9857
|
+
console.log(body.error);
|
|
9858
|
+
if (body.screenshot)
|
|
9859
|
+
printResultScreenshot(body.screenshot);
|
|
9860
|
+
if (!body.ok)
|
|
9861
|
+
process.exitCode = 1;
|
|
9862
|
+
} catch (error) {
|
|
9863
|
+
fail("action batch", error);
|
|
9864
|
+
}
|
|
9865
|
+
});
|
|
9399
9866
|
var setup = program2.command("setup").description("Install Appium and the platform driver (xcuitest / uiautomator2)");
|
|
9400
9867
|
setup.command("ios").description("Ensure Appium + xcuitest; optionally build and install WDA on a physical device").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--device <udid>", "Physical device UDID to install WebDriverAgent on").option("--kind <kind>", "Device kind: physical | simulator", "physical").option("--xcode <path>", "Xcode Contents/Developer path (DEVELOPER_DIR)").option("--team <teamId>", "Apple Development team ID").option("--identity <name>", 'Codesigning identity name, e.g. "Apple Development: …"').option("--force", "Force a full WebDriverAgent rebuild even when prep is reusable").action(async (options) => {
|
|
9401
9868
|
try {
|
|
@@ -10022,7 +10489,7 @@ runsCmd.command("delete").argument("<runId>", "Run id").option("--base-url <url>
|
|
|
10022
10489
|
fail("runs delete", error);
|
|
10023
10490
|
}
|
|
10024
10491
|
});
|
|
10025
|
-
runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android (defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--screen-mode <mode>", "vision | tree — what the agent sees: screenshot only, or also the accessibility tree (default: vision)").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
|
|
10492
|
+
runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android (defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--screen-mode <mode>", "vision | tree — what the agent sees: screenshot only, or also the accessibility tree (default: vision)").option("--lane <lane>", "appium | direct | auto — Android / iOS sim auto → Direct").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
|
|
10026
10493
|
try {
|
|
10027
10494
|
const c = client(options.baseUrl);
|
|
10028
10495
|
const resolved = await resolveAppId(c, appArg);
|
|
@@ -10058,6 +10525,10 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
10058
10525
|
if (options.screenMode && !screenMode) {
|
|
10059
10526
|
throw new Error("--screen-mode must be vision or tree");
|
|
10060
10527
|
}
|
|
10528
|
+
const lane = options.lane === "appium" || options.lane === "direct" || options.lane === "auto" ? options.lane : undefined;
|
|
10529
|
+
if (options.lane && !lane) {
|
|
10530
|
+
throw new Error("--lane must be appium, direct, or auto");
|
|
10531
|
+
}
|
|
10061
10532
|
let run = await c.createRun({
|
|
10062
10533
|
appId: resolved.id,
|
|
10063
10534
|
caseIds,
|
|
@@ -10066,7 +10537,8 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
10066
10537
|
buildId: options.buildId,
|
|
10067
10538
|
buildPath: options.buildPath,
|
|
10068
10539
|
executionMode,
|
|
10069
|
-
screenMode
|
|
10540
|
+
screenMode,
|
|
10541
|
+
lane
|
|
10070
10542
|
});
|
|
10071
10543
|
if (options.wait) {
|
|
10072
10544
|
try {
|
|
@@ -10245,6 +10717,58 @@ scriptCmd.command("run").argument("<file>", "Path to a .yoqa.json / CaseScript J
|
|
|
10245
10717
|
fail("script run", error);
|
|
10246
10718
|
}
|
|
10247
10719
|
});
|
|
10720
|
+
program2.command("benchmark").description("Compare Yoqa (and Argent, when installed) tap-to-result latency on one device").requiredOption("--device <id>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--repeats <n>", "Tap-to-result repeats (default: suite repeats)", (v) => Number(v)).option("--suite <path>", "Versioned suite JSON (default: built-in v1)").option("--tools <list>", "Comma-separated: yoqa,argent (default: yoqa)", "yoqa").option("--lane <lane>", "Yoqa Lane: appium | direct | auto", "auto").option("--arms <list>", "Yoqa Screen arms: vision,tree (default: suite arms)").option("--app <id>", "Catalog App id for Case arms (required when the suite has cases)").option("--out <path>", "Write the JSON report to this path").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print JSON instead of the table").action(async (options) => {
|
|
10721
|
+
try {
|
|
10722
|
+
const platform = options.platform;
|
|
10723
|
+
if (platform !== "ios" && platform !== "android") {
|
|
10724
|
+
throw new Error("--platform must be ios or android");
|
|
10725
|
+
}
|
|
10726
|
+
const lane = options.lane === "appium" || options.lane === "direct" || options.lane === "auto" ? options.lane : undefined;
|
|
10727
|
+
if (options.lane && !lane)
|
|
10728
|
+
throw new Error("--lane must be appium, direct, or auto");
|
|
10729
|
+
const suite = options.suite ? parseBenchmarkSuite(JSON.parse(await readFile2(options.suite, "utf8"))) : DEFAULT_BENCHMARK_SUITE;
|
|
10730
|
+
const arms = options.arms ? parseBenchmarkArms(options.arms.split(",").map((item) => item.trim())) : suite.arms;
|
|
10731
|
+
const repeats = options.repeats && options.repeats > 0 ? options.repeats : suite.repeats;
|
|
10732
|
+
const tools = options.tools.split(",").map((t) => t.trim());
|
|
10733
|
+
const c = client(options.baseUrl);
|
|
10734
|
+
const runs = [];
|
|
10735
|
+
if (tools.includes("yoqa")) {
|
|
10736
|
+
for (const screenMode of arms) {
|
|
10737
|
+
runs.push(await runLatencyScenario(yoqaBenchmarkDriver(c, {
|
|
10738
|
+
deviceId: options.device,
|
|
10739
|
+
platform,
|
|
10740
|
+
lane,
|
|
10741
|
+
screenMode,
|
|
10742
|
+
appId: options.app
|
|
10743
|
+
}), { repeats, platform, suite, screenMode }));
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
if (tools.includes("argent")) {
|
|
10747
|
+
console.error("Argent driver: install the argent CLI and re-run; skipping for now.");
|
|
10748
|
+
}
|
|
10749
|
+
const report = summarizeLatency({
|
|
10750
|
+
recordedAt: new Date().toISOString(),
|
|
10751
|
+
repeats,
|
|
10752
|
+
suiteVersion: suite.version,
|
|
10753
|
+
runs
|
|
10754
|
+
});
|
|
10755
|
+
const recommendation = recommendScreenDefault(report.results.find((row) => row.screenMode === "vision"), report.results.find((row) => row.screenMode === "tree"));
|
|
10756
|
+
if (options.out) {
|
|
10757
|
+
await writeFile3(options.out, `${JSON.stringify({ ...report, recommendation }, null, 2)}
|
|
10758
|
+
`);
|
|
10759
|
+
}
|
|
10760
|
+
if (options.json)
|
|
10761
|
+
console.log(JSON.stringify({ ...report, recommendation }, null, 2));
|
|
10762
|
+
else {
|
|
10763
|
+
console.log(formatLatencyTable(report));
|
|
10764
|
+
if (arms.includes("tree")) {
|
|
10765
|
+
console.log(`default ${recommendation.screenMode} — ${recommendation.reason}`);
|
|
10766
|
+
}
|
|
10767
|
+
}
|
|
10768
|
+
} catch (error) {
|
|
10769
|
+
fail("benchmark", error);
|
|
10770
|
+
}
|
|
10771
|
+
});
|
|
10248
10772
|
function buildProgram() {
|
|
10249
10773
|
return program2;
|
|
10250
10774
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoqa/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.30",
|
|
4
4
|
"description": "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"health": "bun run src/main.ts health"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@yoqa/runner": "0.3.
|
|
41
|
+
"@yoqa/runner": "0.3.30"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^22.15.30",
|