vibeview 0.38.0 → 0.39.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.
|
@@ -15,6 +15,31 @@ import {
|
|
|
15
15
|
uploadAppBuild
|
|
16
16
|
} from "./chunk-MYXUGCR5.js";
|
|
17
17
|
|
|
18
|
+
// src/lib/formatters/not-checked.ts
|
|
19
|
+
function notCheckedSteps(steps) {
|
|
20
|
+
return (steps ?? []).filter((s) => s.status === "not_checked");
|
|
21
|
+
}
|
|
22
|
+
function runStepsNotChecked(result) {
|
|
23
|
+
if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
|
|
24
|
+
return notCheckedSteps(result.steps).length;
|
|
25
|
+
}
|
|
26
|
+
function stepsNotCheckedLabel(n) {
|
|
27
|
+
return `${n} step${n === 1 ? "" : "s"} not checked`;
|
|
28
|
+
}
|
|
29
|
+
function notCheckedSuffix(status, n) {
|
|
30
|
+
return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
|
|
31
|
+
}
|
|
32
|
+
function outcomeOf(r) {
|
|
33
|
+
if (r.not_checked === true || r.status === "not_checked") return "not_checked";
|
|
34
|
+
if (r.not_run === true || r.status === "cancelled" && isNotRunMessage(r.error_message)) {
|
|
35
|
+
return "not_run";
|
|
36
|
+
}
|
|
37
|
+
return r.status ?? "";
|
|
38
|
+
}
|
|
39
|
+
function isNotRunMessage(message) {
|
|
40
|
+
return (message ?? "").startsWith("Not run:");
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
// src/lib/dev-config.ts
|
|
19
44
|
import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
20
45
|
import { join } from "path";
|
|
@@ -5168,6 +5193,11 @@ async function runAgentVerb(cmd, positionals2, opts) {
|
|
|
5168
5193
|
}
|
|
5169
5194
|
|
|
5170
5195
|
export {
|
|
5196
|
+
notCheckedSteps,
|
|
5197
|
+
runStepsNotChecked,
|
|
5198
|
+
stepsNotCheckedLabel,
|
|
5199
|
+
notCheckedSuffix,
|
|
5200
|
+
outcomeOf,
|
|
5171
5201
|
DEV_PLATFORMS,
|
|
5172
5202
|
PLATFORM_NEEDS_METRO,
|
|
5173
5203
|
isDevPlatform,
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
listBuilds,
|
|
16
16
|
loadDevConfig,
|
|
17
17
|
newTransitionState,
|
|
18
|
+
notCheckedSteps,
|
|
19
|
+
notCheckedSuffix,
|
|
20
|
+
outcomeOf,
|
|
18
21
|
platformLabel,
|
|
19
22
|
platformWithArticle,
|
|
20
23
|
pressButtonsHelp,
|
|
@@ -24,8 +27,10 @@ import {
|
|
|
24
27
|
readDevStateFile,
|
|
25
28
|
readMetroState,
|
|
26
29
|
recordDevUse,
|
|
27
|
-
runAgentVerb
|
|
28
|
-
|
|
30
|
+
runAgentVerb,
|
|
31
|
+
runStepsNotChecked,
|
|
32
|
+
stepsNotCheckedLabel
|
|
33
|
+
} from "./chunk-EQN3X62U.js";
|
|
29
34
|
import {
|
|
30
35
|
ApiClient,
|
|
31
36
|
ApiStatusError,
|
|
@@ -61,21 +66,6 @@ import chalk2 from "chalk";
|
|
|
61
66
|
// src/lib/formatters/human.ts
|
|
62
67
|
import chalk from "chalk";
|
|
63
68
|
|
|
64
|
-
// src/lib/formatters/not-checked.ts
|
|
65
|
-
function notCheckedSteps(steps) {
|
|
66
|
-
return (steps ?? []).filter((s) => s.status === "not_checked");
|
|
67
|
-
}
|
|
68
|
-
function runStepsNotChecked(result) {
|
|
69
|
-
if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
|
|
70
|
-
return notCheckedSteps(result.steps).length;
|
|
71
|
-
}
|
|
72
|
-
function stepsNotCheckedLabel(n) {
|
|
73
|
-
return `${n} step${n === 1 ? "" : "s"} not checked`;
|
|
74
|
-
}
|
|
75
|
-
function notCheckedSuffix(status, n) {
|
|
76
|
-
return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
69
|
// src/lib/formatters/cases.ts
|
|
80
70
|
function resultCases(result) {
|
|
81
71
|
if (Array.isArray(result.test_runs)) return result.test_runs;
|
|
@@ -91,6 +81,8 @@ function resultCases(result) {
|
|
|
91
81
|
screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
|
|
92
82
|
url: result.url,
|
|
93
83
|
quarantined: result.quarantined,
|
|
84
|
+
not_checked: result.not_checked,
|
|
85
|
+
not_run: result.not_run,
|
|
94
86
|
steps_not_checked: runStepsNotChecked(result),
|
|
95
87
|
not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
|
|
96
88
|
step: s.step,
|
|
@@ -118,19 +110,21 @@ function failureLines(failure) {
|
|
|
118
110
|
}
|
|
119
111
|
function formatHuman(result, verbose) {
|
|
120
112
|
const lines = [];
|
|
121
|
-
const
|
|
113
|
+
const outcome = outcomeOf(result);
|
|
114
|
+
const icon = outcome === "not_checked" ? chalk.gray("NOT CHECKED") : result.status === "passed" ? chalk.green("PASS") : result.status === "failed" && result.quarantined === true ? chalk.yellow("FAIL (quarantined)") : chalk.red("FAIL");
|
|
122
115
|
const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
123
116
|
lines.push("");
|
|
124
117
|
const title = result.suite_name || result.case_name || result.run_id;
|
|
125
118
|
const cases = resultCases(result);
|
|
126
119
|
const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
|
|
127
|
-
const runSuffix = notCheckedSuffix(
|
|
120
|
+
const runSuffix = notCheckedSuffix(outcome, notChecked);
|
|
128
121
|
lines.push(
|
|
129
122
|
` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
|
|
130
123
|
);
|
|
131
|
-
const passed = cases.filter((t) => t
|
|
124
|
+
const passed = cases.filter((t) => outcomeOf(t) === "passed").length;
|
|
125
|
+
const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
|
|
132
126
|
const total = cases.length;
|
|
133
|
-
const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
|
|
127
|
+
const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
|
|
134
128
|
lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
|
|
135
129
|
if (result.url) {
|
|
136
130
|
lines.push(` ${chalk.dim(result.url)}`);
|
|
@@ -139,16 +133,19 @@ function formatHuman(result, verbose) {
|
|
|
139
133
|
if (suiteCases.length > 0) {
|
|
140
134
|
lines.push("");
|
|
141
135
|
for (const tr of suiteCases) {
|
|
142
|
-
const
|
|
136
|
+
const caseOutcome = outcomeOf(tr);
|
|
137
|
+
const caseIcon = caseOutcome === "not_checked" ? chalk.gray(" NOT CHECKED") : caseOutcome === "not_run" ? chalk.gray(" NOT RUN") : tr.status === "passed" ? chalk.green(" PASS") : tr.status === "failed" && tr.quarantined === true ? chalk.yellow(" FAIL (quarantined)") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
|
|
143
138
|
const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
144
|
-
const caseSuffix = notCheckedSuffix(
|
|
139
|
+
const caseSuffix = notCheckedSuffix(caseOutcome, tr.steps_not_checked);
|
|
145
140
|
lines.push(
|
|
146
141
|
` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
|
|
147
142
|
);
|
|
148
143
|
if (tr.url) {
|
|
149
144
|
lines.push(` ${chalk.dim(tr.url)}`);
|
|
150
145
|
}
|
|
151
|
-
if (
|
|
146
|
+
if (caseOutcome === "not_checked" || caseOutcome === "not_run") {
|
|
147
|
+
if (tr.error_message) lines.push(` ${chalk.dim(tr.error_message)}`);
|
|
148
|
+
} else if (tr.status !== "passed") {
|
|
152
149
|
lines.push(...failureLines(tr));
|
|
153
150
|
}
|
|
154
151
|
}
|
|
@@ -174,7 +171,10 @@ function formatHuman(result, verbose) {
|
|
|
174
171
|
}
|
|
175
172
|
}
|
|
176
173
|
}
|
|
177
|
-
if (result.error_message) {
|
|
174
|
+
if (result.error_message && outcome === "not_checked") {
|
|
175
|
+
lines.push(`
|
|
176
|
+
${chalk.dim(result.error_message)}`);
|
|
177
|
+
} else if (result.error_message) {
|
|
178
178
|
lines.push(`
|
|
179
179
|
${chalk.red("Error:")} ${result.error_message}`);
|
|
180
180
|
}
|
|
@@ -216,13 +216,18 @@ function caseDetailLines(c) {
|
|
|
216
216
|
return lines;
|
|
217
217
|
}
|
|
218
218
|
var isQuarantinedFailure = (t) => t.status === "failed" && t.quarantined === true;
|
|
219
|
+
var NEUTRAL = /* @__PURE__ */ new Set(["not_checked", "not_run"]);
|
|
220
|
+
function neutralMessage(outcome, message) {
|
|
221
|
+
if (outcome === "not_checked") return `not checked: ${message || "no step was checked"}`;
|
|
222
|
+
return message || "Not run";
|
|
223
|
+
}
|
|
219
224
|
function formatJunit(result) {
|
|
220
225
|
const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
|
|
221
226
|
const cases = resultCases(result);
|
|
222
227
|
const tests = cases.length;
|
|
223
228
|
const failures = cases.filter((t) => t.status === "failed" && !isQuarantinedFailure(t)).length;
|
|
224
229
|
const errors = cases.filter((t) => t.status === "error").length;
|
|
225
|
-
const skipped = cases.filter(isQuarantinedFailure).length;
|
|
230
|
+
const skipped = cases.filter((t) => isQuarantinedFailure(t) || NEUTRAL.has(outcomeOf(t))).length;
|
|
226
231
|
const skippedAttr = skipped > 0 ? ` skipped="${skipped}"` : "";
|
|
227
232
|
const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
|
|
228
233
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -249,8 +254,9 @@ function formatJunit(result) {
|
|
|
249
254
|
for (const tr of cases) {
|
|
250
255
|
const caseName = escapeXml(tr.case_name);
|
|
251
256
|
const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
|
|
252
|
-
const
|
|
253
|
-
|
|
257
|
+
const outcome = outcomeOf(tr);
|
|
258
|
+
const detail = caseDetailLines({ ...tr, status: outcome });
|
|
259
|
+
if (outcome === "passed" && detail.length === 0) {
|
|
254
260
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
|
|
255
261
|
continue;
|
|
256
262
|
}
|
|
@@ -258,6 +264,10 @@ function formatJunit(result) {
|
|
|
258
264
|
if (isQuarantinedFailure(tr)) {
|
|
259
265
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
260
266
|
lines.push(` <skipped message="quarantined: ${msg}" />`);
|
|
267
|
+
} else if (NEUTRAL.has(outcome)) {
|
|
268
|
+
lines.push(
|
|
269
|
+
` <skipped message="${escapeXml(neutralMessage(outcome, tr.error_message))}" />`
|
|
270
|
+
);
|
|
261
271
|
} else if (tr.status === "failed") {
|
|
262
272
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
263
273
|
lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
|
|
@@ -294,7 +304,7 @@ function formatJunitError(name, message, url) {
|
|
|
294
304
|
lines.push("</testsuites>");
|
|
295
305
|
return lines.join("\n");
|
|
296
306
|
}
|
|
297
|
-
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
307
|
+
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
|
|
298
308
|
function caseLines(c, classname) {
|
|
299
309
|
const name = escapeXml(c.name);
|
|
300
310
|
const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
|
|
@@ -331,6 +341,14 @@ function formatMultiSuiteJunit(result, opts = {}) {
|
|
|
331
341
|
url: c.url || void 0,
|
|
332
342
|
steps_not_checked: c.steps_not_checked
|
|
333
343
|
};
|
|
344
|
+
const outcome = outcomeOf(c);
|
|
345
|
+
if (NEUTRAL.has(outcome)) {
|
|
346
|
+
return {
|
|
347
|
+
...base,
|
|
348
|
+
verdict: "skipped",
|
|
349
|
+
message: neutralMessage(outcome, c.error_message)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
334
352
|
if (c.status === "failed" && c.quarantined === true) {
|
|
335
353
|
return {
|
|
336
354
|
...base,
|
|
@@ -409,6 +427,7 @@ function formatMultiSuiteJunit(result, opts = {}) {
|
|
|
409
427
|
}
|
|
410
428
|
|
|
411
429
|
// src/lib/poll.ts
|
|
430
|
+
var TERMINAL_STATUSES = ["passed", "failed", "error", "cancelled", "not_checked"];
|
|
412
431
|
function resumeSpinner(spinner, text) {
|
|
413
432
|
if (spinner.isEnabled === false) {
|
|
414
433
|
if (text !== void 0) spinner.text = text;
|
|
@@ -506,20 +525,26 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
506
525
|
`${chalk2.cyan("\u25B6")} ${chalk2.bold(tr.case_name || "Case")} started`
|
|
507
526
|
);
|
|
508
527
|
}
|
|
509
|
-
const isTerminal =
|
|
510
|
-
const wasTerminal = prev &&
|
|
528
|
+
const isTerminal = TERMINAL_STATUSES.includes(cur);
|
|
529
|
+
const wasTerminal = prev && TERMINAL_STATUSES.includes(prev);
|
|
511
530
|
if (isTerminal && !wasTerminal) {
|
|
512
|
-
const
|
|
531
|
+
const outcome = outcomeOf(tr);
|
|
532
|
+
const neutral = outcome === "not_checked" || outcome === "not_run";
|
|
533
|
+
const icon = neutral ? chalk2.gray("\u25CB") : cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
534
|
+
const label = neutral ? outcome.replace("_", " ") : cur;
|
|
513
535
|
const dur = tr.duration_ms ? ` (${(tr.duration_ms / 1e3).toFixed(1)}s)` : "";
|
|
514
|
-
printBelowSpinner(
|
|
515
|
-
|
|
536
|
+
printBelowSpinner(
|
|
537
|
+
spinner,
|
|
538
|
+
`${icon} ${chalk2.bold(tr.case_name || "Case")} ${label}${dur}`
|
|
539
|
+
);
|
|
540
|
+
if (tr.error_message && cur !== "passed" && !neutral) {
|
|
516
541
|
printBelowSpinner(spinner, ` ${chalk2.red(tr.error_message)}`);
|
|
517
542
|
}
|
|
518
543
|
}
|
|
519
544
|
caseStateBefore.set(tr.public_id, cur);
|
|
520
545
|
}
|
|
521
546
|
}
|
|
522
|
-
if (
|
|
547
|
+
if (TERMINAL_STATUSES.includes(result.status)) {
|
|
523
548
|
if (printedSteps.size > 0 && Array.isArray(result.steps)) {
|
|
524
549
|
spinner.stop();
|
|
525
550
|
result.steps.forEach((st, i) => {
|
|
@@ -569,7 +594,7 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
569
594
|
if (result.type === "suite_run" && Array.isArray(result.test_runs)) {
|
|
570
595
|
const total = result.total_cases ?? result.test_runs.length;
|
|
571
596
|
const done = result.test_runs.filter(
|
|
572
|
-
(tr) =>
|
|
597
|
+
(tr) => TERMINAL_STATUSES.includes(tr.status ?? "")
|
|
573
598
|
).length;
|
|
574
599
|
spinner.text = `Running suite \u2014 ${done}/${total} cases complete...`;
|
|
575
600
|
} else {
|
|
@@ -666,10 +691,10 @@ function displayProgressEvent(evt, ctx) {
|
|
|
666
691
|
break;
|
|
667
692
|
}
|
|
668
693
|
case "run_completed": {
|
|
669
|
-
const icon = evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
694
|
+
const icon = evt.status === "not_checked" ? chalk2.gray("\u25CB") : evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
670
695
|
spinner.stop();
|
|
671
696
|
process.stderr.write(
|
|
672
|
-
`${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
697
|
+
`${icon} Run ${evt.status === "not_checked" ? "not checked" : evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
673
698
|
`
|
|
674
699
|
);
|
|
675
700
|
resumeSpinner(spinner);
|
|
@@ -709,7 +734,7 @@ function outputResult(result, options) {
|
|
|
709
734
|
}
|
|
710
735
|
}
|
|
711
736
|
function mapExitCode(status) {
|
|
712
|
-
if (status === "passed") return EXIT_PASS;
|
|
737
|
+
if (status === "passed" || status === "not_checked") return EXIT_PASS;
|
|
713
738
|
if (status === "failed") return EXIT_TEST_FAIL;
|
|
714
739
|
return EXIT_INFRA_ERROR;
|
|
715
740
|
}
|
|
@@ -832,7 +857,8 @@ ${signal} received.
|
|
|
832
857
|
device: options.device,
|
|
833
858
|
build: options.build,
|
|
834
859
|
metadata: options.metadata,
|
|
835
|
-
|
|
860
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
861
|
+
retryFailed: options.retry === true,
|
|
836
862
|
platform: options.platform,
|
|
837
863
|
app: options.app
|
|
838
864
|
});
|
|
@@ -960,7 +986,8 @@ ${signal} received.
|
|
|
960
986
|
device_id: opts.device,
|
|
961
987
|
build_id: opts.build,
|
|
962
988
|
metadata: opts.metadata,
|
|
963
|
-
|
|
989
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
990
|
+
retry_failed: opts.retry === true
|
|
964
991
|
});
|
|
965
992
|
} catch (err) {
|
|
966
993
|
spinner.stop();
|
|
@@ -1021,7 +1048,7 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1021
1048
|
pending = [];
|
|
1022
1049
|
if (nextText !== null) spinner.start(nextText);
|
|
1023
1050
|
};
|
|
1024
|
-
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
1051
|
+
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
|
|
1025
1052
|
let last = null;
|
|
1026
1053
|
const abandon = async (reason) => {
|
|
1027
1054
|
flush(null);
|
|
@@ -1080,10 +1107,11 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1080
1107
|
}
|
|
1081
1108
|
const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
|
|
1082
1109
|
if (TERMINAL.has(cur) && !wasTerminal) {
|
|
1083
|
-
const
|
|
1110
|
+
const nothingChecked = outcomeOf(sr) === "not_checked";
|
|
1111
|
+
const mark = nothingChecked ? chalk5.gray("\u25CB") : cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
|
|
1084
1112
|
const link = sr.url ? chalk5.dim(" " + sr.url) : "";
|
|
1085
1113
|
print(
|
|
1086
|
-
`${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr
|
|
1114
|
+
`${mark} ${chalk5.bold(sr.suite_name)} ${nothingChecked ? "not checked" : cur}: ${suiteCounts(sr)}${link}`
|
|
1087
1115
|
);
|
|
1088
1116
|
if (sr.error_message) {
|
|
1089
1117
|
print(` ${chalk5.red(sr.error_message)}`);
|
|
@@ -1137,39 +1165,48 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1137
1165
|
0
|
|
1138
1166
|
);
|
|
1139
1167
|
const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
|
|
1168
|
+
const aggregate = outcomeOf(last) === "not_checked" ? "not checked" : last.status;
|
|
1140
1169
|
process.stdout.write(`
|
|
1141
|
-
Aggregate: ${
|
|
1170
|
+
Aggregate: ${aggregate}${aggregateNote}
|
|
1142
1171
|
`);
|
|
1143
1172
|
for (const sr of last.suite_runs) {
|
|
1144
|
-
const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
1173
|
+
const mark = outcomeOf(sr) === "not_checked" ? chalk5.gray("\u25CB") : sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
1145
1174
|
const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
|
|
1146
|
-
process.stdout.write(
|
|
1147
|
-
|
|
1148
|
-
`
|
|
1149
|
-
);
|
|
1175
|
+
process.stdout.write(` ${mark} ${sr.suite_name}: ${suiteCounts(sr)}${link}
|
|
1176
|
+
`);
|
|
1150
1177
|
if (sr.error_message) {
|
|
1151
1178
|
process.stdout.write(` ${chalk5.red(sr.error_message)}
|
|
1152
1179
|
`);
|
|
1153
1180
|
}
|
|
1154
1181
|
for (const cr of sr.case_runs ?? []) {
|
|
1155
|
-
const
|
|
1182
|
+
const outcome = outcomeOf(cr);
|
|
1183
|
+
const neutral = outcome === "not_checked" || outcome === "not_run";
|
|
1184
|
+
const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
|
|
1156
1185
|
const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
|
|
1157
|
-
const unchecked = notCheckedSuffix(
|
|
1186
|
+
const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
|
|
1187
|
+
const label = neutral ? outcome.replace("_", " ") : cr.status ?? "?";
|
|
1158
1188
|
process.stdout.write(
|
|
1159
|
-
` ${caseMark} ${cr.case_name ?? "Case"} ${
|
|
1189
|
+
` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
|
|
1160
1190
|
`
|
|
1161
1191
|
);
|
|
1162
|
-
if (cr.error_message &&
|
|
1192
|
+
if (cr.error_message && neutral) {
|
|
1193
|
+
process.stdout.write(` ${chalk5.dim(cr.error_message)}
|
|
1194
|
+
`);
|
|
1195
|
+
} else if (cr.error_message && cr.status !== "passed") {
|
|
1163
1196
|
process.stdout.write(` ${chalk5.red(cr.error_message)}
|
|
1164
1197
|
`);
|
|
1165
1198
|
}
|
|
1166
1199
|
}
|
|
1167
1200
|
}
|
|
1168
1201
|
}
|
|
1169
|
-
if (last.status === "passed") process.exit(EXIT_PASS);
|
|
1202
|
+
if (last.status === "passed" || last.status === "not_checked") process.exit(EXIT_PASS);
|
|
1170
1203
|
if (last.status === "failed") process.exit(EXIT_TEST_FAIL);
|
|
1171
1204
|
process.exit(EXIT_INFRA_ERROR);
|
|
1172
1205
|
}
|
|
1206
|
+
function suiteCounts(sr) {
|
|
1207
|
+
const nc = sr.not_checked_tests ?? 0;
|
|
1208
|
+
return `${sr.passed} passed, ${sr.failed} failed${nc > 0 ? `, ${nc} not checked` : ""}`;
|
|
1209
|
+
}
|
|
1173
1210
|
|
|
1174
1211
|
// src/commands/run-test.ts
|
|
1175
1212
|
import chalk6 from "chalk";
|
|
@@ -1238,7 +1275,8 @@ ${signal} received.
|
|
|
1238
1275
|
device: options.device,
|
|
1239
1276
|
build: options.build,
|
|
1240
1277
|
metadata: options.metadata,
|
|
1241
|
-
|
|
1278
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
1279
|
+
retryFailed: options.retry === true,
|
|
1242
1280
|
platform: options.platform,
|
|
1243
1281
|
app: options.app
|
|
1244
1282
|
});
|
|
@@ -1641,7 +1679,7 @@ function formatImport(result, baseUrl) {
|
|
|
1641
1679
|
}
|
|
1642
1680
|
for (const f of result.flags) lines.push(flagLine(f));
|
|
1643
1681
|
for (const t of [...result.tests, ...result.setups]) {
|
|
1644
|
-
const meta = [`${t.steps.length} steps`];
|
|
1682
|
+
const meta = [`${t.steps.length} ${t.steps.length === 1 ? "step" : "steps"}`];
|
|
1645
1683
|
if (t.setup_keys.length) meta.push(`before this test: ${t.setup_keys.join(", ")}`);
|
|
1646
1684
|
if (t.start_state) meta.push(`start: ${START_STATE_LABEL[t.start_state] ?? t.start_state}`);
|
|
1647
1685
|
const head = t.importable ? t.name : chalk11.red(`${t.name} (not imported)`);
|
|
@@ -4040,9 +4078,9 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
|
|
|
4040
4078
|
"--app <app-id>",
|
|
4041
4079
|
"Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
|
|
4042
4080
|
).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
|
|
4043
|
-
"--
|
|
4044
|
-
"
|
|
4045
|
-
).action((suiteId, opts) => {
|
|
4081
|
+
"--retry",
|
|
4082
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4083
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((suiteId, opts) => {
|
|
4046
4084
|
if (opts.device && opts.deviceCategory) {
|
|
4047
4085
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
4048
4086
|
process.exit(2);
|
|
@@ -4079,9 +4117,9 @@ program.command("run-suites").description("Run all suites matching a tag filter"
|
|
|
4079
4117
|
"--device <device_id>",
|
|
4080
4118
|
"Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
|
|
4081
4119
|
).option("--build <id>", "AppBuild id (validated against every matched suite)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
|
|
4082
|
-
"--
|
|
4083
|
-
"
|
|
4084
|
-
).action((opts) => {
|
|
4120
|
+
"--retry",
|
|
4121
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4122
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((opts) => {
|
|
4085
4123
|
if (opts.device && opts.deviceCategory) {
|
|
4086
4124
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
4087
4125
|
process.exit(2);
|
|
@@ -4115,9 +4153,9 @@ program.command("run-test <test-id>").description("Run a single test case").opti
|
|
|
4115
4153
|
"--app <app-id>",
|
|
4116
4154
|
"Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
|
|
4117
4155
|
).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
|
|
4118
|
-
"--
|
|
4119
|
-
"
|
|
4120
|
-
).action((testId, opts) => {
|
|
4156
|
+
"--retry",
|
|
4157
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4158
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((testId, opts) => {
|
|
4121
4159
|
if (opts.device && opts.deviceCategory) {
|
|
4122
4160
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
4123
4161
|
process.exit(2);
|
|
@@ -4349,7 +4387,7 @@ program.command("dev-reload").description(
|
|
|
4349
4387
|
program.command("mcp").description(
|
|
4350
4388
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
4351
4389
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
4352
|
-
const { runMcpServer } = await import("./server-
|
|
4390
|
+
const { runMcpServer } = await import("./server-GEY4WZUH.js");
|
|
4353
4391
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
4354
4392
|
});
|
|
4355
4393
|
program.command("agent-setup").description(
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isDevPlatform,
|
|
21
21
|
loadDevConfig,
|
|
22
22
|
loadLocalDevConfig,
|
|
23
|
+
outcomeOf,
|
|
23
24
|
parseBuildId,
|
|
24
25
|
parseIdleWarningSeconds,
|
|
25
26
|
platformWithArticle,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
renderEnvelope,
|
|
29
30
|
resolveMetro,
|
|
30
31
|
startDevSession
|
|
31
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-EQN3X62U.js";
|
|
32
33
|
import {
|
|
33
34
|
ApiClient,
|
|
34
35
|
NotFoundError,
|
|
@@ -399,33 +400,40 @@ function notCheckedNote(status, n) {
|
|
|
399
400
|
return ` \u2014 ${n} step${n === 1 ? "" : "s"} not checked (no recorded actions; not verified)`;
|
|
400
401
|
}
|
|
401
402
|
var RUN_IN_PROGRESS = /* @__PURE__ */ new Set(["queued", "pending", "running"]);
|
|
403
|
+
function agentStatus(r) {
|
|
404
|
+
const outcome = outcomeOf(r);
|
|
405
|
+
return outcome === "not_checked" || outcome === "not_run" ? outcome.replace("_", " ") : outcome;
|
|
406
|
+
}
|
|
407
|
+
function testsNotChecked(n) {
|
|
408
|
+
return typeof n === "number" && n > 0 ? `, ${n} not checked` : "";
|
|
409
|
+
}
|
|
402
410
|
function formatRunForAgent(run) {
|
|
403
411
|
const lines = [];
|
|
404
412
|
if (run.lanes) {
|
|
405
413
|
lines.push(
|
|
406
|
-
`${run.name}: ${run
|
|
414
|
+
`${run.name}: ${agentStatus(run)} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed${testsNotChecked(run.summary.tests_not_checked)}`
|
|
407
415
|
);
|
|
408
416
|
for (const lane of run.lanes) {
|
|
409
417
|
const detail = [lane.platform, lane.os_version].filter(Boolean).join(" ");
|
|
410
|
-
let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${lane
|
|
418
|
+
let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${agentStatus(lane)}`;
|
|
411
419
|
if (lane.status === "queued" && typeof lane.queue?.position === "number") {
|
|
412
420
|
head += ` (position ${lane.queue.position} in the queue)`;
|
|
413
421
|
}
|
|
414
422
|
if (lane.error_message) head += ` \u2014 ${lane.error_message}`;
|
|
415
423
|
lines.push(head);
|
|
416
424
|
for (const r of lane.runs ?? []) {
|
|
417
|
-
const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed)` : "";
|
|
425
|
+
const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed${testsNotChecked(r.tests_not_checked)})` : "";
|
|
418
426
|
lines.push(
|
|
419
|
-
` ${r.name}: ${r
|
|
427
|
+
` ${r.name}: ${agentStatus(r)}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
|
|
420
428
|
);
|
|
421
429
|
}
|
|
422
430
|
}
|
|
423
431
|
} else if (Array.isArray(run.test_runs)) {
|
|
424
|
-
lines.push(`${run.suite_name ?? "Group"}: ${run
|
|
432
|
+
lines.push(`${run.suite_name ?? "Group"}: ${agentStatus(run)}`);
|
|
425
433
|
for (const t of run.test_runs) {
|
|
426
434
|
const q = t.quarantined && t.status === "failed" ? " (quarantined)" : "";
|
|
427
435
|
lines.push(
|
|
428
|
-
` ${t.case_name}: ${t
|
|
436
|
+
` ${t.case_name}: ${agentStatus(t)}${q}${t.error_message ? ` \u2014 ${t.error_message}` : ""}${notCheckedNote(outcomeOf(t), t.steps_not_checked)}`
|
|
429
437
|
);
|
|
430
438
|
}
|
|
431
439
|
} else {
|
|
@@ -434,7 +442,7 @@ function formatRunForAgent(run) {
|
|
|
434
442
|
const notChecked = typeof run.steps_not_checked === "number" ? run.steps_not_checked : (run.steps ?? []).filter((s) => s.status === "not_checked").length;
|
|
435
443
|
const q = run.quarantined && run.status === "failed" ? " (quarantined)" : "";
|
|
436
444
|
lines.push(
|
|
437
|
-
`${name}: ${run
|
|
445
|
+
`${name}: ${agentStatus(run)}${q}${run.error_message ? ` \u2014 ${run.error_message}` : ""}${notCheckedNote(outcomeOf(run), notChecked)}`
|
|
438
446
|
);
|
|
439
447
|
if (failed) {
|
|
440
448
|
lines.push(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeview",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
|
|
5
5
|
"mcpName": "io.github.vibeview/vibeview",
|
|
6
6
|
"type": "module",
|