@yoqa/cli 0.3.16 → 0.3.17
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/README.md +1 -1
- package/dist/main.js +515 -73
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,4 +18,4 @@ yoqa serve
|
|
|
18
18
|
|
|
19
19
|
Disable auto-start with `YOQA_NO_AUTOSTART=1`. Override the URL with `YOQA_RUNNER_HOST` / `YOQA_RUNNER_PORT` / `YOQA_RUNNER_URL`.
|
|
20
20
|
|
|
21
|
-
See [CLI docs](https://yoqa.mintlify.app/docs/cli).
|
|
21
|
+
See [CLI docs](https://yoqa.mintlify.app/docs/cli) and [GitHub Actions](https://yoqa.mintlify.app/docs/github-actions).
|
package/dist/main.js
CHANGED
|
@@ -2122,7 +2122,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
2122
2122
|
});
|
|
2123
2123
|
|
|
2124
2124
|
// src/main.ts
|
|
2125
|
-
import { readFile as readFile2
|
|
2125
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
2126
2126
|
|
|
2127
2127
|
// ../../node_modules/zod/v3/external.js
|
|
2128
2128
|
var exports_external = {};
|
|
@@ -6293,13 +6293,24 @@ var caseFlowStepSchema = exports_external.object({
|
|
|
6293
6293
|
flowId: exports_external.string().min(1).nullable().optional()
|
|
6294
6294
|
});
|
|
6295
6295
|
var caseRunStatusSchema = exports_external.union([exports_external.literal("passed"), exports_external.literal("errored")]);
|
|
6296
|
-
var
|
|
6297
|
-
exports_external.
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6296
|
+
var caseScriptTapSchema = exports_external.object({
|
|
6297
|
+
type: exports_external.literal("tap"),
|
|
6298
|
+
x: exports_external.number().min(0).max(1000).optional(),
|
|
6299
|
+
y: exports_external.number().min(0).max(1000).optional(),
|
|
6300
|
+
label: exports_external.string().min(1).optional(),
|
|
6301
|
+
id: exports_external.string().min(1).optional(),
|
|
6302
|
+
reason: exports_external.string().optional()
|
|
6303
|
+
}).superRefine((value, ctx) => {
|
|
6304
|
+
const hasCoords = value.x != null && value.y != null;
|
|
6305
|
+
if (!hasCoords && !value.label && !value.id) {
|
|
6306
|
+
ctx.addIssue({
|
|
6307
|
+
code: exports_external.ZodIssueCode.custom,
|
|
6308
|
+
message: "tap requires x/y or label or id"
|
|
6309
|
+
});
|
|
6310
|
+
}
|
|
6311
|
+
});
|
|
6312
|
+
var caseScriptActionSchema = exports_external.union([
|
|
6313
|
+
caseScriptTapSchema,
|
|
6303
6314
|
exports_external.object({
|
|
6304
6315
|
type: exports_external.literal("type"),
|
|
6305
6316
|
text: exports_external.string(),
|
|
@@ -6309,6 +6320,13 @@ var caseScriptActionSchema = exports_external.discriminatedUnion("type", [
|
|
|
6309
6320
|
type: exports_external.literal("wait"),
|
|
6310
6321
|
ms: exports_external.number().min(0).max(1e4),
|
|
6311
6322
|
reason: exports_external.string().optional()
|
|
6323
|
+
}),
|
|
6324
|
+
exports_external.object({
|
|
6325
|
+
type: exports_external.literal("assert"),
|
|
6326
|
+
assertion: exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).default("visible"),
|
|
6327
|
+
text: exports_external.string().min(1),
|
|
6328
|
+
timeoutMs: exports_external.number().min(0).max(120000).optional(),
|
|
6329
|
+
reason: exports_external.string().optional()
|
|
6312
6330
|
})
|
|
6313
6331
|
]);
|
|
6314
6332
|
var caseScriptSchema = exports_external.object({
|
|
@@ -6716,18 +6734,103 @@ var createBuildRequestSchema = exports_external.object({
|
|
|
6716
6734
|
var listBuildsResponseSchema = exports_external.object({
|
|
6717
6735
|
builds: exports_external.array(buildSchema)
|
|
6718
6736
|
});
|
|
6737
|
+
|
|
6738
|
+
// ../runner-client/src/wait-for-run.ts
|
|
6739
|
+
var DEFAULT_WAIT_FOR_RUN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
6740
|
+
var DEFAULT_WAIT_FOR_RUN_INTERVAL_MS = 2000;
|
|
6741
|
+
function isTerminalRunStatus(status) {
|
|
6742
|
+
return status === "passed" || status === "errored" || status === "cancelled";
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6745
|
+
class WaitForRunTimeoutError extends Error {
|
|
6746
|
+
runId;
|
|
6747
|
+
lastRun;
|
|
6748
|
+
timeoutMs;
|
|
6749
|
+
constructor(runId, timeoutMs, lastRun) {
|
|
6750
|
+
const lastStatus = lastRun?.status ?? "unknown";
|
|
6751
|
+
super(`Timed out after ${timeoutMs}ms waiting for run ${runId} (last status: ${lastStatus})`);
|
|
6752
|
+
this.name = "WaitForRunTimeoutError";
|
|
6753
|
+
this.runId = runId;
|
|
6754
|
+
this.lastRun = lastRun;
|
|
6755
|
+
this.timeoutMs = timeoutMs;
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6759
|
+
class WaitForRunAbortedError extends Error {
|
|
6760
|
+
runId;
|
|
6761
|
+
lastRun;
|
|
6762
|
+
constructor(runId, lastRun) {
|
|
6763
|
+
super(`Wait for run ${runId} was aborted`);
|
|
6764
|
+
this.name = "WaitForRunAbortedError";
|
|
6765
|
+
this.runId = runId;
|
|
6766
|
+
this.lastRun = lastRun;
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6769
|
+
function defaultSleep(ms) {
|
|
6770
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6771
|
+
}
|
|
6772
|
+
async function waitForRun(client, runId, options = {}) {
|
|
6773
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_FOR_RUN_TIMEOUT_MS;
|
|
6774
|
+
const intervalMs = options.intervalMs ?? DEFAULT_WAIT_FOR_RUN_INTERVAL_MS;
|
|
6775
|
+
if (!(timeoutMs > 0)) {
|
|
6776
|
+
throw new Error("timeoutMs must be positive");
|
|
6777
|
+
}
|
|
6778
|
+
if (intervalMs < 0) {
|
|
6779
|
+
throw new Error("intervalMs must be non-negative");
|
|
6780
|
+
}
|
|
6781
|
+
const now = options.now ?? Date.now;
|
|
6782
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
6783
|
+
const deadline = now() + timeoutMs;
|
|
6784
|
+
let lastRun = null;
|
|
6785
|
+
while (now() < deadline) {
|
|
6786
|
+
if (options.signal?.aborted) {
|
|
6787
|
+
throw new WaitForRunAbortedError(runId, lastRun);
|
|
6788
|
+
}
|
|
6789
|
+
lastRun = await client.getRun(runId);
|
|
6790
|
+
options.onStatus?.(lastRun);
|
|
6791
|
+
if (isTerminalRunStatus(lastRun.status)) {
|
|
6792
|
+
return lastRun;
|
|
6793
|
+
}
|
|
6794
|
+
const remaining = deadline - now();
|
|
6795
|
+
if (remaining <= 0)
|
|
6796
|
+
break;
|
|
6797
|
+
await sleep(Math.min(intervalMs, remaining));
|
|
6798
|
+
}
|
|
6799
|
+
if (options.signal?.aborted) {
|
|
6800
|
+
throw new WaitForRunAbortedError(runId, lastRun);
|
|
6801
|
+
}
|
|
6802
|
+
if (lastRun && isTerminalRunStatus(lastRun.status)) {
|
|
6803
|
+
return lastRun;
|
|
6804
|
+
}
|
|
6805
|
+
throw new WaitForRunTimeoutError(runId, timeoutMs, lastRun);
|
|
6806
|
+
}
|
|
6807
|
+
function pickLatestRun(runs) {
|
|
6808
|
+
if (runs.length === 0)
|
|
6809
|
+
return null;
|
|
6810
|
+
return runs.reduce((best, run) => run.createdAt > best.createdAt ? run : best);
|
|
6811
|
+
}
|
|
6719
6812
|
// ../runner-client/src/run-report.ts
|
|
6720
6813
|
function actionSummary(action) {
|
|
6721
6814
|
if (!action || typeof action !== "object")
|
|
6722
6815
|
return "Step";
|
|
6723
6816
|
const record = action;
|
|
6724
6817
|
const type = typeof record.type === "string" ? record.type : "step";
|
|
6725
|
-
if (type === "tap")
|
|
6818
|
+
if (type === "tap") {
|
|
6819
|
+
if (typeof record.label === "string" && record.label.trim())
|
|
6820
|
+
return `Tap: ${record.label}`;
|
|
6821
|
+
if (typeof record.id === "string" && record.id.trim())
|
|
6822
|
+
return `Tap #${record.id}`;
|
|
6726
6823
|
return "Tap";
|
|
6824
|
+
}
|
|
6727
6825
|
if (type === "type")
|
|
6728
6826
|
return `Type${typeof record.text === "string" ? `: ${record.text}` : ""}`;
|
|
6729
6827
|
if (type === "wait")
|
|
6730
6828
|
return "Wait";
|
|
6829
|
+
if (type === "assert") {
|
|
6830
|
+
const assertion = typeof record.assertion === "string" ? record.assertion : "visible";
|
|
6831
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
6832
|
+
return text ? `Assert ${assertion}: ${text}` : `Assert ${assertion}`;
|
|
6833
|
+
}
|
|
6731
6834
|
if (type === "verify")
|
|
6732
6835
|
return "Verify";
|
|
6733
6836
|
if (type === "done")
|
|
@@ -7012,6 +7115,54 @@ function formatRunReportMarkdown(doc) {
|
|
|
7012
7115
|
return lines.join(`
|
|
7013
7116
|
`);
|
|
7014
7117
|
}
|
|
7118
|
+
function formatRunReportGithubSummary(doc) {
|
|
7119
|
+
const accent = statusAccent(doc.status);
|
|
7120
|
+
const passed = doc.tests.filter((test) => test.status === "passed").length;
|
|
7121
|
+
const failed = doc.tests.filter((test) => test.status === "errored").length;
|
|
7122
|
+
const cancelled = doc.tests.filter((test) => test.status === "cancelled").length;
|
|
7123
|
+
const failedSteps = doc.tests.flatMap((test) => test.steps.filter((step) => !step.ok).map((step) => ({ test, step })));
|
|
7124
|
+
const lines = [
|
|
7125
|
+
`## Yoqa Report — ${doc.title}`,
|
|
7126
|
+
"",
|
|
7127
|
+
`**${accent.label}** · ${doc.tests.length} test${doc.tests.length === 1 ? "" : "s"} · ${passed} passed · ${failed} failed · ${cancelled} cancelled`,
|
|
7128
|
+
"",
|
|
7129
|
+
"| Field | Value |",
|
|
7130
|
+
"| --- | --- |",
|
|
7131
|
+
`| App | ${escapeMd(doc.appLabel ?? "—")} |`,
|
|
7132
|
+
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
7133
|
+
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
7134
|
+
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
7135
|
+
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
7136
|
+
`| Run ID | \`${doc.id}\` |`,
|
|
7137
|
+
""
|
|
7138
|
+
];
|
|
7139
|
+
if (doc.error) {
|
|
7140
|
+
lines.push(`> **Error:** ${doc.error}`, "");
|
|
7141
|
+
}
|
|
7142
|
+
if (doc.tests.length > 0) {
|
|
7143
|
+
lines.push("### Tests", "", "| # | Test | Status | Steps |", "| --- | --- | --- | --- |");
|
|
7144
|
+
doc.tests.forEach((test, testIndex) => {
|
|
7145
|
+
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
7146
|
+
lines.push(`| ${testIndex + 1} | ${escapeMd(test.title)} | ${testLabel} | ${test.steps.length} |`);
|
|
7147
|
+
});
|
|
7148
|
+
lines.push("");
|
|
7149
|
+
}
|
|
7150
|
+
if (failedSteps.length > 0) {
|
|
7151
|
+
lines.push("### Failed steps", "");
|
|
7152
|
+
for (const { test, step } of failedSteps) {
|
|
7153
|
+
lines.push(`- **${escapeMd(test.title)}** · step ${step.index}: ${escapeMd(step.summary)}`);
|
|
7154
|
+
if (step.reason)
|
|
7155
|
+
lines.push(` - Reason: ${escapeMd(step.reason)}`);
|
|
7156
|
+
if (step.detail && step.detail !== step.reason) {
|
|
7157
|
+
lines.push(` - Detail: ${escapeMd(step.detail)}`);
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7160
|
+
lines.push("");
|
|
7161
|
+
}
|
|
7162
|
+
lines.push("Download the `yoqa-report` artifact for the full HTML report with screenshots.", "");
|
|
7163
|
+
return lines.join(`
|
|
7164
|
+
`);
|
|
7165
|
+
}
|
|
7015
7166
|
// ../runner-client/src/shell-script.ts
|
|
7016
7167
|
function shellSingleQuote(value) {
|
|
7017
7168
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -7719,6 +7870,9 @@ class RunnerClient {
|
|
|
7719
7870
|
const json = await this.requestJson(`/runs/${encodeURIComponent(runId)}`, undefined, "Get run failed");
|
|
7720
7871
|
return runSchema.parse(json);
|
|
7721
7872
|
}
|
|
7873
|
+
async waitForRun(runId, options) {
|
|
7874
|
+
return waitForRun(this, runId, options);
|
|
7875
|
+
}
|
|
7722
7876
|
async deleteRun(runId) {
|
|
7723
7877
|
await this.requestJson(`/runs/${encodeURIComponent(runId)}`, { method: "DELETE" }, "Delete run failed");
|
|
7724
7878
|
}
|
|
@@ -7850,7 +8004,7 @@ var {
|
|
|
7850
8004
|
// package.json
|
|
7851
8005
|
var package_default = {
|
|
7852
8006
|
name: "@yoqa/cli",
|
|
7853
|
-
version: "0.3.
|
|
8007
|
+
version: "0.3.17",
|
|
7854
8008
|
description: "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
|
|
7855
8009
|
license: "UNLICENSED",
|
|
7856
8010
|
type: "module",
|
|
@@ -7888,7 +8042,7 @@ var package_default = {
|
|
|
7888
8042
|
health: "bun run src/main.ts health"
|
|
7889
8043
|
},
|
|
7890
8044
|
dependencies: {
|
|
7891
|
-
"@yoqa/runner": "0.3.
|
|
8045
|
+
"@yoqa/runner": "0.3.17"
|
|
7892
8046
|
},
|
|
7893
8047
|
devDependencies: {
|
|
7894
8048
|
"@types/node": "^22.15.30",
|
|
@@ -7902,6 +8056,15 @@ var package_default = {
|
|
|
7902
8056
|
}
|
|
7903
8057
|
};
|
|
7904
8058
|
|
|
8059
|
+
// src/catalog-app.ts
|
|
8060
|
+
function findCatalogApp(apps, prefixOrId) {
|
|
8061
|
+
const needle = prefixOrId.trim();
|
|
8062
|
+
if (!needle)
|
|
8063
|
+
return;
|
|
8064
|
+
const lower = needle.toLowerCase();
|
|
8065
|
+
return apps.find((app) => app.prefix === needle) ?? apps.find((app) => app.prefix.toLowerCase() === lower) ?? apps.find((app) => app.id === needle);
|
|
8066
|
+
}
|
|
8067
|
+
|
|
7905
8068
|
// src/doctor-table.ts
|
|
7906
8069
|
var ANSI = {
|
|
7907
8070
|
reset: "\x1B[0m",
|
|
@@ -8369,6 +8532,206 @@ function commandChainIncludes(command, name) {
|
|
|
8369
8532
|
return false;
|
|
8370
8533
|
}
|
|
8371
8534
|
|
|
8535
|
+
// src/report.ts
|
|
8536
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
|
|
8537
|
+
import { dirname as dirname3 } from "node:path";
|
|
8538
|
+
|
|
8539
|
+
// src/github.ts
|
|
8540
|
+
import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
|
|
8541
|
+
import { dirname as dirname2 } from "node:path";
|
|
8542
|
+
function isGithubActions(env = process.env) {
|
|
8543
|
+
return env.GITHUB_ACTIONS === "true";
|
|
8544
|
+
}
|
|
8545
|
+
function resolveGithubWritePath(flag, filePath, env = process.env) {
|
|
8546
|
+
if (flag === false)
|
|
8547
|
+
return null;
|
|
8548
|
+
const path = filePath?.trim();
|
|
8549
|
+
if (!path)
|
|
8550
|
+
return null;
|
|
8551
|
+
if (flag === true || isGithubActions(env))
|
|
8552
|
+
return path;
|
|
8553
|
+
return null;
|
|
8554
|
+
}
|
|
8555
|
+
function formatGithubOutputLines(fields) {
|
|
8556
|
+
const lines = [];
|
|
8557
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
8558
|
+
if (value.includes(`
|
|
8559
|
+
`) || value.includes("\r")) {
|
|
8560
|
+
throw new Error(`GitHub output ${key} cannot contain newlines`);
|
|
8561
|
+
}
|
|
8562
|
+
lines.push(`${key}=${value}`);
|
|
8563
|
+
}
|
|
8564
|
+
return `${lines.join(`
|
|
8565
|
+
`)}
|
|
8566
|
+
`;
|
|
8567
|
+
}
|
|
8568
|
+
async function appendGithubFile(path, contents) {
|
|
8569
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
8570
|
+
const body = contents.endsWith(`
|
|
8571
|
+
`) ? contents : `${contents}
|
|
8572
|
+
`;
|
|
8573
|
+
await appendFile(path, body, "utf8");
|
|
8574
|
+
}
|
|
8575
|
+
|
|
8576
|
+
// src/report.ts
|
|
8577
|
+
var DEFAULT_RUN_WAIT_TIMEOUT_SECONDS = 1800;
|
|
8578
|
+
function normalizeReportFormat(raw) {
|
|
8579
|
+
const format = raw === "md" || raw === "markdown" ? "md" : raw ?? "html";
|
|
8580
|
+
if (format !== "html" && format !== "md") {
|
|
8581
|
+
throw new Error("--format must be html or md");
|
|
8582
|
+
}
|
|
8583
|
+
return format;
|
|
8584
|
+
}
|
|
8585
|
+
function parseFailOn(raw) {
|
|
8586
|
+
const value = (raw ?? "never").trim().toLowerCase();
|
|
8587
|
+
if (value === "errored" || value === "never")
|
|
8588
|
+
return value;
|
|
8589
|
+
throw new Error("--fail-on must be errored or never");
|
|
8590
|
+
}
|
|
8591
|
+
function parseTimeoutSeconds(raw, fallback = DEFAULT_RUN_WAIT_TIMEOUT_SECONDS) {
|
|
8592
|
+
const n = raw == null || raw.trim() === "" ? fallback : Number(raw);
|
|
8593
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
8594
|
+
throw new Error("--timeout must be a positive number of seconds");
|
|
8595
|
+
}
|
|
8596
|
+
return n;
|
|
8597
|
+
}
|
|
8598
|
+
function shouldFailOnRunStatus(failOn, status) {
|
|
8599
|
+
return failOn === "errored" && status === "errored";
|
|
8600
|
+
}
|
|
8601
|
+
async function resolveReportRunId(client, runIdArg, latestAppId, latestLabel) {
|
|
8602
|
+
const runId = runIdArg?.trim();
|
|
8603
|
+
if (runId && latestAppId) {
|
|
8604
|
+
throw new Error("Pass either <runId> or --latest <app>, not both");
|
|
8605
|
+
}
|
|
8606
|
+
if (runId)
|
|
8607
|
+
return runId;
|
|
8608
|
+
if (!latestAppId) {
|
|
8609
|
+
throw new Error("Provide a run id or --latest <app>");
|
|
8610
|
+
}
|
|
8611
|
+
const latest = pickLatestRun(await client.listRuns(latestAppId));
|
|
8612
|
+
if (!latest) {
|
|
8613
|
+
throw new Error(`No runs found for app ${latestLabel}`);
|
|
8614
|
+
}
|
|
8615
|
+
return latest.id;
|
|
8616
|
+
}
|
|
8617
|
+
async function loadFinishedRun(client, runId, options, deps) {
|
|
8618
|
+
const run = await client.getRun(runId);
|
|
8619
|
+
if (isTerminalRunStatus(run.status))
|
|
8620
|
+
return run;
|
|
8621
|
+
if (!options.wait) {
|
|
8622
|
+
throw new Error(`Run is still ${run.status}. Wait until it finishes (passed / errored / cancelled) or pass --wait.`);
|
|
8623
|
+
}
|
|
8624
|
+
const timeoutSeconds = parseTimeoutSeconds(options.timeout);
|
|
8625
|
+
try {
|
|
8626
|
+
return await waitForRun(client, runId, {
|
|
8627
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
8628
|
+
onStatus: deps.onWaitStatus,
|
|
8629
|
+
sleep: deps.sleep,
|
|
8630
|
+
now: deps.now
|
|
8631
|
+
});
|
|
8632
|
+
} catch (error) {
|
|
8633
|
+
if (error instanceof WaitForRunTimeoutError && error.lastRun) {
|
|
8634
|
+
await writeRunGithubOutput(error.lastRun, undefined, deps, options);
|
|
8635
|
+
}
|
|
8636
|
+
throw error;
|
|
8637
|
+
}
|
|
8638
|
+
}
|
|
8639
|
+
async function writeRunGithubOutput(run, reportPath, deps, options) {
|
|
8640
|
+
const env = deps.env ?? process.env;
|
|
8641
|
+
const outputPath = resolveGithubWritePath(options.githubOutput, env.GITHUB_OUTPUT, env);
|
|
8642
|
+
if (!outputPath)
|
|
8643
|
+
return;
|
|
8644
|
+
const fields = {
|
|
8645
|
+
run_id: run.id,
|
|
8646
|
+
status: run.status
|
|
8647
|
+
};
|
|
8648
|
+
if (reportPath)
|
|
8649
|
+
fields.report_path = reportPath;
|
|
8650
|
+
const append = deps.appendGithubFile ?? appendGithubFile;
|
|
8651
|
+
await append(outputPath, formatGithubOutputLines(fields));
|
|
8652
|
+
}
|
|
8653
|
+
async function exportCatalogRunReport(client, runId, options, deps = {}) {
|
|
8654
|
+
const format = normalizeReportFormat(options.format);
|
|
8655
|
+
const failOn = parseFailOn(options.failOn);
|
|
8656
|
+
const env = deps.env ?? process.env;
|
|
8657
|
+
const log = deps.log ?? console.log;
|
|
8658
|
+
const write = deps.writeFile ?? writeFile2;
|
|
8659
|
+
const makeDir = deps.mkdir ?? mkdir3;
|
|
8660
|
+
const append = deps.appendGithubFile ?? appendGithubFile;
|
|
8661
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
8662
|
+
const run = await loadFinishedRun(client, runId, options, deps);
|
|
8663
|
+
const screenshotsByStepId = {};
|
|
8664
|
+
const steps = run.tests.flatMap((test) => test.steps ?? []);
|
|
8665
|
+
await Promise.all(steps.map(async (step) => {
|
|
8666
|
+
if (!step.screenshotUri)
|
|
8667
|
+
return;
|
|
8668
|
+
const response = await fetchImpl(client.getRunStepScreenshotUrl(run.id, step.id));
|
|
8669
|
+
if (!response.ok)
|
|
8670
|
+
return;
|
|
8671
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
8672
|
+
screenshotsByStepId[step.id] = Buffer.from(bytes).toString("base64");
|
|
8673
|
+
}));
|
|
8674
|
+
const apps = await client.listApps();
|
|
8675
|
+
const app = apps.find((row) => row.id === run.appId);
|
|
8676
|
+
const cases = await client.listCases(run.appId);
|
|
8677
|
+
const caseTitles = {};
|
|
8678
|
+
for (const item of cases) {
|
|
8679
|
+
caseTitles[item.id] = `#${item.number} ${item.name}`;
|
|
8680
|
+
}
|
|
8681
|
+
let deviceLabel = run.deviceId;
|
|
8682
|
+
try {
|
|
8683
|
+
const devices = await client.listDevices(run.platform, { includeUnavailable: true });
|
|
8684
|
+
const device = devices.devices.find((row) => row.id === run.deviceId);
|
|
8685
|
+
if (device) {
|
|
8686
|
+
deviceLabel = `${device.name} · ${run.platform} ${device.osVersion}`;
|
|
8687
|
+
}
|
|
8688
|
+
} catch {}
|
|
8689
|
+
const doc = buildRunReportFromCatalogRun(run, {
|
|
8690
|
+
appLabel: app ? `${app.prefix} — ${app.name}` : run.appId,
|
|
8691
|
+
deviceLabel,
|
|
8692
|
+
caseTitles
|
|
8693
|
+
}, screenshotsByStepId);
|
|
8694
|
+
const contents = format === "html" ? formatRunReportHtml(doc) : formatRunReportMarkdown(doc);
|
|
8695
|
+
const extension = format === "html" ? "html" : "md";
|
|
8696
|
+
const outputPath = options.output?.trim() || `${suggestedRunReportBasename(doc)}.${extension}`;
|
|
8697
|
+
await makeDir(dirname3(outputPath), { recursive: true });
|
|
8698
|
+
await write(outputPath, contents, "utf8");
|
|
8699
|
+
log(`wrote ${outputPath} (${doc.status}, ${steps.length} steps)`);
|
|
8700
|
+
const summaryPath = resolveGithubWritePath(options.githubSummary, env.GITHUB_STEP_SUMMARY, env);
|
|
8701
|
+
if (summaryPath) {
|
|
8702
|
+
await append(summaryPath, formatRunReportGithubSummary(doc));
|
|
8703
|
+
}
|
|
8704
|
+
await writeRunGithubOutput(run, outputPath, { ...deps, env, appendGithubFile: append }, options);
|
|
8705
|
+
return {
|
|
8706
|
+
outputPath,
|
|
8707
|
+
status: doc.status,
|
|
8708
|
+
runId: run.id,
|
|
8709
|
+
stepCount: steps.length,
|
|
8710
|
+
shouldFail: shouldFailOnRunStatus(failOn, doc.status)
|
|
8711
|
+
};
|
|
8712
|
+
}
|
|
8713
|
+
function attachReportCommandOptions(cmd) {
|
|
8714
|
+
return cmd.argument("[runId]", "Run id").option("--latest <app>", "Export the newest run for this app prefix or id").option("--format <format>", "html | md (default: html)", "html").option("-o, --output <path>", "Output file path (default: yoqa-run-<id>-<status>.html|md)").option("--wait", "Wait until the run finishes before exporting").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-summary", "Append compact markdown to $GITHUB_STEP_SUMMARY").option("--no-github-summary", "Do not write a GitHub job summary").option("--github-output", "Write run_id, status, report_path to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").option("--fail-on <when>", "errored | never (default: never)", "never");
|
|
8715
|
+
}
|
|
8716
|
+
function createWaitStatusLogger(write = console.error) {
|
|
8717
|
+
let last = "";
|
|
8718
|
+
return (run) => {
|
|
8719
|
+
if (run.status === last)
|
|
8720
|
+
return;
|
|
8721
|
+
last = run.status;
|
|
8722
|
+
write(`run ${run.id} ${run.status}`);
|
|
8723
|
+
};
|
|
8724
|
+
}
|
|
8725
|
+
async function maybeWriteGithubRunOutput(run, flag, env = process.env, append = appendGithubFile) {
|
|
8726
|
+
const outputPath = resolveGithubWritePath(flag, env.GITHUB_OUTPUT, env);
|
|
8727
|
+
if (!outputPath)
|
|
8728
|
+
return;
|
|
8729
|
+
await append(outputPath, formatGithubOutputLines({ run_id: run.id, status: run.status }));
|
|
8730
|
+
}
|
|
8731
|
+
function attachGithubOutputOptions(cmd) {
|
|
8732
|
+
return cmd.option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs");
|
|
8733
|
+
}
|
|
8734
|
+
|
|
8372
8735
|
// src/main.ts
|
|
8373
8736
|
function runnerBaseUrl() {
|
|
8374
8737
|
return runnerBaseUrlFromEnv();
|
|
@@ -8423,12 +8786,37 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
8423
8786
|
});
|
|
8424
8787
|
async function resolveAppId(c, prefixOrId) {
|
|
8425
8788
|
const apps = await c.listApps();
|
|
8426
|
-
const match =
|
|
8789
|
+
const match = findCatalogApp(apps, prefixOrId);
|
|
8427
8790
|
if (!match) {
|
|
8428
8791
|
throw new Error(`App not found: ${prefixOrId}. Run: yoqa apps list`);
|
|
8429
8792
|
}
|
|
8430
8793
|
return { id: match.id, prefix: match.prefix, name: match.name };
|
|
8431
8794
|
}
|
|
8795
|
+
async function loadCaseScriptFile(file) {
|
|
8796
|
+
const raw = await readFile2(file, "utf8");
|
|
8797
|
+
const parsed = caseScriptSchema.safeParse(JSON.parse(raw));
|
|
8798
|
+
if (!parsed.success) {
|
|
8799
|
+
throw new Error(`Invalid CaseScript: ${parsed.error.message}`);
|
|
8800
|
+
}
|
|
8801
|
+
return parsed.data;
|
|
8802
|
+
}
|
|
8803
|
+
async function handleReportCommand(commandLabel, runIdArg, options) {
|
|
8804
|
+
try {
|
|
8805
|
+
const c = client(options.baseUrl);
|
|
8806
|
+
let latestAppId;
|
|
8807
|
+
if (options.latest) {
|
|
8808
|
+
latestAppId = (await resolveAppId(c, options.latest)).id;
|
|
8809
|
+
}
|
|
8810
|
+
const runId = await resolveReportRunId(c, runIdArg, latestAppId, options.latest ?? "");
|
|
8811
|
+
const result = await exportCatalogRunReport(c, runId, options, {
|
|
8812
|
+
onWaitStatus: createWaitStatusLogger()
|
|
8813
|
+
});
|
|
8814
|
+
if (result.shouldFail)
|
|
8815
|
+
process.exitCode = 1;
|
|
8816
|
+
} catch (error) {
|
|
8817
|
+
fail(commandLabel, error);
|
|
8818
|
+
}
|
|
8819
|
+
}
|
|
8432
8820
|
program2.command("health").description("Check that the local runner is reachable").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).action(async (options) => {
|
|
8433
8821
|
try {
|
|
8434
8822
|
const health = await client(options.baseUrl).health();
|
|
@@ -8877,6 +9265,25 @@ appsCmd.command("list").option("--base-url <url>", "Runner base URL", runnerBase
|
|
|
8877
9265
|
fail("apps list", error);
|
|
8878
9266
|
}
|
|
8879
9267
|
});
|
|
9268
|
+
appsCmd.command("create").description("Create a local catalog app").requiredOption("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix (allocated from name when omitted)").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (options) => {
|
|
9269
|
+
try {
|
|
9270
|
+
const c = client(options.baseUrl);
|
|
9271
|
+
let app = await c.createApp({ name: options.name, prefix: options.prefix });
|
|
9272
|
+
if (options.iosBundleId || options.androidApplicationId) {
|
|
9273
|
+
app = await c.updateApp(app.id, {
|
|
9274
|
+
iosBundleId: options.iosBundleId,
|
|
9275
|
+
androidApplicationId: options.androidApplicationId
|
|
9276
|
+
});
|
|
9277
|
+
}
|
|
9278
|
+
if (options.json) {
|
|
9279
|
+
console.log(JSON.stringify(app, null, 2));
|
|
9280
|
+
return;
|
|
9281
|
+
}
|
|
9282
|
+
console.log(`created ${app.prefix}`);
|
|
9283
|
+
} catch (error) {
|
|
9284
|
+
fail("apps create", error);
|
|
9285
|
+
}
|
|
9286
|
+
});
|
|
8880
9287
|
appsCmd.command("get").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
8881
9288
|
try {
|
|
8882
9289
|
const c = client(options.baseUrl);
|
|
@@ -8960,15 +9367,20 @@ casesCmd.command("get").argument("<app>", "App prefix or id").argument("<number>
|
|
|
8960
9367
|
fail("cases get", error);
|
|
8961
9368
|
}
|
|
8962
9369
|
});
|
|
8963
|
-
casesCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--title <title>", "Case title").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
9370
|
+
casesCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--title <title>", "Case title").option("--script-file <path>", "Path to a .yoqa.json CaseScript to attach").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
8964
9371
|
try {
|
|
8965
9372
|
const c = client(options.baseUrl);
|
|
8966
9373
|
const resolved = await resolveAppId(c, appArg);
|
|
8967
|
-
|
|
9374
|
+
let created = await c.createCase(resolved.id, {
|
|
8968
9375
|
name: options.title,
|
|
8969
9376
|
flows: [],
|
|
8970
9377
|
tags: []
|
|
8971
9378
|
});
|
|
9379
|
+
if (options.scriptFile) {
|
|
9380
|
+
created = await c.updateCase(created.id, {
|
|
9381
|
+
script: await loadCaseScriptFile(options.scriptFile)
|
|
9382
|
+
});
|
|
9383
|
+
}
|
|
8972
9384
|
if (options.json) {
|
|
8973
9385
|
console.log(JSON.stringify(created, null, 2));
|
|
8974
9386
|
return;
|
|
@@ -8978,7 +9390,7 @@ casesCmd.command("create").argument("<app>", "App prefix or id").requiredOption(
|
|
|
8978
9390
|
fail("cases create", error);
|
|
8979
9391
|
}
|
|
8980
9392
|
});
|
|
8981
|
-
casesCmd.command("update").argument("<app>", "App prefix or id").argument("<number>", "Case number", (v) => Number(v)).option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--title <title>", "New title").option("--json", "Print raw JSON").action(async (appArg, number, options) => {
|
|
9393
|
+
casesCmd.command("update").argument("<app>", "App prefix or id").argument("<number>", "Case number", (v) => Number(v)).option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--title <title>", "New title").option("--script-file <path>", "Path to a .yoqa.json CaseScript to attach").option("--json", "Print raw JSON").action(async (appArg, number, options) => {
|
|
8982
9394
|
try {
|
|
8983
9395
|
const c = client(options.baseUrl);
|
|
8984
9396
|
const resolved = await resolveAppId(c, appArg);
|
|
@@ -8986,7 +9398,10 @@ casesCmd.command("update").argument("<app>", "App prefix or id").argument("<numb
|
|
|
8986
9398
|
const found = cases.find((item) => item.number === number);
|
|
8987
9399
|
if (!found)
|
|
8988
9400
|
throw new Error(`Case #${number} not found`);
|
|
8989
|
-
const updated = await c.updateCase(found.id, {
|
|
9401
|
+
const updated = await c.updateCase(found.id, {
|
|
9402
|
+
name: options.title,
|
|
9403
|
+
script: options.scriptFile ? await loadCaseScriptFile(options.scriptFile) : undefined
|
|
9404
|
+
});
|
|
8990
9405
|
if (options.json) {
|
|
8991
9406
|
console.log(JSON.stringify(updated, null, 2));
|
|
8992
9407
|
return;
|
|
@@ -9151,6 +9566,9 @@ buildsCmd.command("delete").argument("<buildId>", "Build id").option("--base-url
|
|
|
9151
9566
|
fail("builds delete", error);
|
|
9152
9567
|
}
|
|
9153
9568
|
});
|
|
9569
|
+
attachReportCommandOptions(program2.command("report").description("Export a detailed HTML or Markdown report with step screenshots")).option("--base-url <url>", "Runner base URL", runnerBaseUrl()).action(async (runId, options) => {
|
|
9570
|
+
await handleReportCommand("report", runId, options);
|
|
9571
|
+
});
|
|
9154
9572
|
var runsCmd = program2.command("runs").description("Create and inspect local agent runs");
|
|
9155
9573
|
runsCmd.command("list").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
9156
9574
|
try {
|
|
@@ -9183,55 +9601,28 @@ runsCmd.command("get").argument("<runId>", "Run id").option("--base-url <url>",
|
|
|
9183
9601
|
fail("runs get", error);
|
|
9184
9602
|
}
|
|
9185
9603
|
});
|
|
9186
|
-
runsCmd.command("report").description("Export a detailed HTML or Markdown report with step screenshots")
|
|
9604
|
+
attachReportCommandOptions(runsCmd.command("report").description("Export a detailed HTML or Markdown report with step screenshots")).option("--base-url <url>", "Runner base URL", runnerBaseUrl()).action(async (runId, options) => {
|
|
9605
|
+
await handleReportCommand("runs report", runId, options);
|
|
9606
|
+
});
|
|
9607
|
+
attachGithubOutputOptions(runsCmd.command("wait").description("Wait until a catalog run finishes").argument("<runId>", "Run id").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS))).option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (runId, options) => {
|
|
9187
9608
|
try {
|
|
9188
|
-
const
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
}
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
const screenshotsByStepId = {};
|
|
9198
|
-
const steps = run.tests.flatMap((test) => test.steps ?? []);
|
|
9199
|
-
await Promise.all(steps.map(async (step) => {
|
|
9200
|
-
if (!step.screenshotUri)
|
|
9201
|
-
return;
|
|
9202
|
-
const response = await fetch(c.getRunStepScreenshotUrl(run.id, step.id));
|
|
9203
|
-
if (!response.ok)
|
|
9204
|
-
return;
|
|
9205
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
9206
|
-
screenshotsByStepId[step.id] = Buffer.from(bytes).toString("base64");
|
|
9207
|
-
}));
|
|
9208
|
-
const apps = await c.listApps();
|
|
9209
|
-
const app = apps.find((row) => row.id === run.appId);
|
|
9210
|
-
const cases = await c.listCases(run.appId);
|
|
9211
|
-
const caseTitles = {};
|
|
9212
|
-
for (const item of cases) {
|
|
9213
|
-
caseTitles[item.id] = `#${item.number} ${item.name}`;
|
|
9609
|
+
const finished = await waitForRun(client(options.baseUrl), runId, {
|
|
9610
|
+
timeoutMs: parseTimeoutSeconds(options.timeout) * 1000,
|
|
9611
|
+
onStatus: createWaitStatusLogger()
|
|
9612
|
+
});
|
|
9613
|
+
await maybeWriteGithubRunOutput(finished, options.githubOutput);
|
|
9614
|
+
if (options.json) {
|
|
9615
|
+
console.log(JSON.stringify(finished, null, 2));
|
|
9616
|
+
} else {
|
|
9617
|
+
console.log(`${finished.id} ${finished.status}`);
|
|
9214
9618
|
}
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
const devices2 = await c.listDevices(run.platform, { includeUnavailable: true });
|
|
9218
|
-
const device = devices2.devices.find((row) => row.id === run.deviceId);
|
|
9219
|
-
if (device) {
|
|
9220
|
-
deviceLabel = `${device.name} · ${run.platform} ${device.osVersion}`;
|
|
9221
|
-
}
|
|
9222
|
-
} catch {}
|
|
9223
|
-
const doc = buildRunReportFromCatalogRun(run, {
|
|
9224
|
-
appLabel: app ? `${app.prefix} — ${app.name}` : run.appId,
|
|
9225
|
-
deviceLabel,
|
|
9226
|
-
caseTitles
|
|
9227
|
-
}, screenshotsByStepId);
|
|
9228
|
-
const contents = format === "html" ? formatRunReportHtml(doc) : formatRunReportMarkdown(doc);
|
|
9229
|
-
const extension = format === "html" ? "html" : "md";
|
|
9230
|
-
const outputPath = options.output?.trim() || `${suggestedRunReportBasename(doc)}.${extension}`;
|
|
9231
|
-
await writeFile2(outputPath, contents, "utf8");
|
|
9232
|
-
console.log(`wrote ${outputPath} (${doc.status}, ${steps.length} steps)`);
|
|
9619
|
+
if (shouldFailOnRunStatus("errored", finished.status))
|
|
9620
|
+
process.exitCode = 1;
|
|
9233
9621
|
} catch (error) {
|
|
9234
|
-
|
|
9622
|
+
if (error instanceof WaitForRunTimeoutError && error.lastRun) {
|
|
9623
|
+
await maybeWriteGithubRunOutput(error.lastRun, options.githubOutput);
|
|
9624
|
+
}
|
|
9625
|
+
fail("runs wait", error);
|
|
9235
9626
|
}
|
|
9236
9627
|
});
|
|
9237
9628
|
runsCmd.command("delete").argument("<runId>", "Run id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).action(async (runId, options) => {
|
|
@@ -9242,7 +9633,7 @@ runsCmd.command("delete").argument("<runId>", "Run id").option("--base-url <url>
|
|
|
9242
9633
|
fail("runs delete", error);
|
|
9243
9634
|
}
|
|
9244
9635
|
});
|
|
9245
|
-
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("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
9636
|
+
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("--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) => {
|
|
9246
9637
|
try {
|
|
9247
9638
|
const c = client(options.baseUrl);
|
|
9248
9639
|
const resolved = await resolveAppId(c, appArg);
|
|
@@ -9274,7 +9665,7 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
9274
9665
|
if (options.mode && !executionMode) {
|
|
9275
9666
|
throw new Error("--mode must be auto, script, or agent");
|
|
9276
9667
|
}
|
|
9277
|
-
|
|
9668
|
+
let run = await c.createRun({
|
|
9278
9669
|
appId: resolved.id,
|
|
9279
9670
|
caseIds,
|
|
9280
9671
|
deviceId,
|
|
@@ -9283,11 +9674,28 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
9283
9674
|
buildPath: options.buildPath,
|
|
9284
9675
|
executionMode
|
|
9285
9676
|
});
|
|
9677
|
+
if (options.wait) {
|
|
9678
|
+
try {
|
|
9679
|
+
run = await waitForRun(c, run.id, {
|
|
9680
|
+
timeoutMs: parseTimeoutSeconds(options.timeout) * 1000,
|
|
9681
|
+
onStatus: createWaitStatusLogger()
|
|
9682
|
+
});
|
|
9683
|
+
} catch (error) {
|
|
9684
|
+
if (error instanceof WaitForRunTimeoutError && error.lastRun) {
|
|
9685
|
+
await maybeWriteGithubRunOutput(error.lastRun, options.githubOutput);
|
|
9686
|
+
}
|
|
9687
|
+
throw error;
|
|
9688
|
+
}
|
|
9689
|
+
}
|
|
9690
|
+
await maybeWriteGithubRunOutput(run, options.githubOutput);
|
|
9286
9691
|
if (options.json) {
|
|
9287
9692
|
console.log(JSON.stringify(run, null, 2));
|
|
9288
|
-
|
|
9693
|
+
} else {
|
|
9694
|
+
console.log(`created ${run.id} (${run.status})`);
|
|
9695
|
+
}
|
|
9696
|
+
if (options.wait && shouldFailOnRunStatus("errored", run.status)) {
|
|
9697
|
+
process.exitCode = 1;
|
|
9289
9698
|
}
|
|
9290
|
-
console.log(`created ${run.id} (${run.status})`);
|
|
9291
9699
|
} catch (error) {
|
|
9292
9700
|
fail("runs create", error);
|
|
9293
9701
|
}
|
|
@@ -9295,17 +9703,12 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
9295
9703
|
var scriptCmd = program2.command("script").description("Replay exported CaseScript files");
|
|
9296
9704
|
scriptCmd.command("run").argument("<file>", "Path to a .yoqa.json / CaseScript JSON file").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON per step").action(async (file, options) => {
|
|
9297
9705
|
try {
|
|
9298
|
-
const
|
|
9299
|
-
const parsed = caseScriptSchema.safeParse(JSON.parse(raw));
|
|
9300
|
-
if (!parsed.success) {
|
|
9301
|
-
throw new Error(`Invalid CaseScript: ${parsed.error.message}`);
|
|
9302
|
-
}
|
|
9706
|
+
const script = await loadCaseScriptFile(file);
|
|
9303
9707
|
const c = client(options.baseUrl);
|
|
9304
9708
|
const active = await c.getActiveDevice();
|
|
9305
9709
|
if (!active) {
|
|
9306
9710
|
throw new Error("No active device session. Connect first: yoqa devices connect <id>");
|
|
9307
9711
|
}
|
|
9308
|
-
const script = parsed.data;
|
|
9309
9712
|
let idx = 0;
|
|
9310
9713
|
for (const action2 of script.actions) {
|
|
9311
9714
|
idx += 1;
|
|
@@ -9319,7 +9722,46 @@ scriptCmd.command("run").argument("<file>", "Path to a .yoqa.json / CaseScript J
|
|
|
9319
9722
|
await sleep(ms);
|
|
9320
9723
|
continue;
|
|
9321
9724
|
}
|
|
9322
|
-
|
|
9725
|
+
if (action2.type === "assert") {
|
|
9726
|
+
const timeoutMs = action2.timeoutMs ?? 5000;
|
|
9727
|
+
const deadline = Date.now() + timeoutMs;
|
|
9728
|
+
for (;; ) {
|
|
9729
|
+
const screen = await c.getScreen();
|
|
9730
|
+
const found = screenHasText(screen.elements, action2.text);
|
|
9731
|
+
if (action2.assertion === "visible" && found)
|
|
9732
|
+
break;
|
|
9733
|
+
if (action2.assertion === "not-visible" && !found)
|
|
9734
|
+
break;
|
|
9735
|
+
if (Date.now() >= deadline) {
|
|
9736
|
+
throw new Error(action2.assertion === "visible" ? `Expected visible text not found within ${Math.round(timeoutMs / 1000)}s: ${action2.text}` : `Unexpected text still visible after ${Math.round(timeoutMs / 1000)}s: ${action2.text}`);
|
|
9737
|
+
}
|
|
9738
|
+
await sleep(400);
|
|
9739
|
+
}
|
|
9740
|
+
if (options.json) {
|
|
9741
|
+
console.log(JSON.stringify({
|
|
9742
|
+
ok: true,
|
|
9743
|
+
kind: "assert",
|
|
9744
|
+
assertion: action2.assertion,
|
|
9745
|
+
text: action2.text,
|
|
9746
|
+
step: idx
|
|
9747
|
+
}, null, 2));
|
|
9748
|
+
} else {
|
|
9749
|
+
console.log(`ok assert ${action2.assertion} (${idx}/${script.actions.length})`);
|
|
9750
|
+
}
|
|
9751
|
+
continue;
|
|
9752
|
+
}
|
|
9753
|
+
const tapBody = { kind: "tap" };
|
|
9754
|
+
if (action2.type === "tap") {
|
|
9755
|
+
if (action2.label)
|
|
9756
|
+
tapBody.label = action2.label;
|
|
9757
|
+
if (action2.id)
|
|
9758
|
+
tapBody.id = action2.id;
|
|
9759
|
+
if (action2.x != null)
|
|
9760
|
+
tapBody.x = action2.x;
|
|
9761
|
+
if (action2.y != null)
|
|
9762
|
+
tapBody.y = action2.y;
|
|
9763
|
+
}
|
|
9764
|
+
const body = action2.type === "tap" ? await c.performAction(tapBody) : await c.performAction({ kind: "input", text: action2.text });
|
|
9323
9765
|
if (options.json) {
|
|
9324
9766
|
console.log(JSON.stringify({ ...body, step: idx }, null, 2));
|
|
9325
9767
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoqa/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
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.17"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^22.15.30",
|