@yoqa/cli 0.3.24 → 0.3.26
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 +1002 -706
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -6299,6 +6299,8 @@ var caseScriptTapSchema = exports_external.object({
|
|
|
6299
6299
|
y: exports_external.number().min(0).max(1000).optional(),
|
|
6300
6300
|
label: exports_external.string().min(1).optional(),
|
|
6301
6301
|
id: exports_external.string().min(1).optional(),
|
|
6302
|
+
double: exports_external.boolean().optional(),
|
|
6303
|
+
durationMs: exports_external.number().min(50).max(5000).optional(),
|
|
6302
6304
|
reason: exports_external.string().optional()
|
|
6303
6305
|
}).superRefine((value, ctx) => {
|
|
6304
6306
|
const hasCoords = value.x != null && value.y != null;
|
|
@@ -6309,8 +6311,45 @@ var caseScriptTapSchema = exports_external.object({
|
|
|
6309
6311
|
});
|
|
6310
6312
|
}
|
|
6311
6313
|
});
|
|
6314
|
+
var caseScriptSwipeSchema = exports_external.object({
|
|
6315
|
+
type: exports_external.literal("swipe"),
|
|
6316
|
+
x: exports_external.number().min(0).max(1000),
|
|
6317
|
+
y: exports_external.number().min(0).max(1000),
|
|
6318
|
+
x2: exports_external.number().min(0).max(1000),
|
|
6319
|
+
y2: exports_external.number().min(0).max(1000),
|
|
6320
|
+
durationMs: exports_external.number().min(50).max(5000).optional(),
|
|
6321
|
+
reason: exports_external.string().optional()
|
|
6322
|
+
});
|
|
6323
|
+
var caseScriptDragSchema = exports_external.object({
|
|
6324
|
+
type: exports_external.literal("drag"),
|
|
6325
|
+
x: exports_external.number().min(0).max(1000),
|
|
6326
|
+
y: exports_external.number().min(0).max(1000),
|
|
6327
|
+
x2: exports_external.number().min(0).max(1000),
|
|
6328
|
+
y2: exports_external.number().min(0).max(1000),
|
|
6329
|
+
durationMs: exports_external.number().min(50).max(5000).optional(),
|
|
6330
|
+
reason: exports_external.string().optional()
|
|
6331
|
+
});
|
|
6332
|
+
var caseScriptAppIdSchema = exports_external.union([
|
|
6333
|
+
exports_external.object({
|
|
6334
|
+
type: exports_external.literal("activate-app"),
|
|
6335
|
+
appId: exports_external.string().min(1),
|
|
6336
|
+
reason: exports_external.string().optional()
|
|
6337
|
+
}),
|
|
6338
|
+
exports_external.object({
|
|
6339
|
+
type: exports_external.literal("terminate-app"),
|
|
6340
|
+
appId: exports_external.string().min(1),
|
|
6341
|
+
reason: exports_external.string().optional()
|
|
6342
|
+
}),
|
|
6343
|
+
exports_external.object({
|
|
6344
|
+
type: exports_external.literal("restart-app"),
|
|
6345
|
+
appId: exports_external.string().min(1),
|
|
6346
|
+
reason: exports_external.string().optional()
|
|
6347
|
+
})
|
|
6348
|
+
]);
|
|
6312
6349
|
var caseScriptActionSchema = exports_external.union([
|
|
6313
6350
|
caseScriptTapSchema,
|
|
6351
|
+
caseScriptSwipeSchema,
|
|
6352
|
+
caseScriptDragSchema,
|
|
6314
6353
|
exports_external.object({
|
|
6315
6354
|
type: exports_external.literal("type"),
|
|
6316
6355
|
text: exports_external.string(),
|
|
@@ -6332,6 +6371,17 @@ var caseScriptActionSchema = exports_external.union([
|
|
|
6332
6371
|
type: exports_external.literal("alert"),
|
|
6333
6372
|
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).default("accept"),
|
|
6334
6373
|
reason: exports_external.string().optional()
|
|
6374
|
+
}),
|
|
6375
|
+
caseScriptAppIdSchema,
|
|
6376
|
+
exports_external.object({
|
|
6377
|
+
type: exports_external.literal("background-app"),
|
|
6378
|
+
seconds: exports_external.number().min(0).max(120).optional(),
|
|
6379
|
+
reason: exports_external.string().optional()
|
|
6380
|
+
}),
|
|
6381
|
+
exports_external.object({
|
|
6382
|
+
type: exports_external.literal("open-url"),
|
|
6383
|
+
url: exports_external.string().min(1),
|
|
6384
|
+
reason: exports_external.string().optional()
|
|
6335
6385
|
})
|
|
6336
6386
|
]);
|
|
6337
6387
|
var caseScriptSchema = exports_external.object({
|
|
@@ -6588,6 +6638,7 @@ var runStepSchema = exports_external.object({
|
|
|
6588
6638
|
ok: exports_external.boolean(),
|
|
6589
6639
|
latencyMs: exports_external.number().nonnegative(),
|
|
6590
6640
|
detail: exports_external.string().nullable(),
|
|
6641
|
+
command: exports_external.string().nullable().optional(),
|
|
6591
6642
|
createdAt: exports_external.number().int().nonnegative()
|
|
6592
6643
|
});
|
|
6593
6644
|
var runTestSchema = exports_external.object({
|
|
@@ -6599,6 +6650,7 @@ var runTestSchema = exports_external.object({
|
|
|
6599
6650
|
error: exports_external.string().nullable(),
|
|
6600
6651
|
startedAt: exports_external.number().int().nonnegative().nullable(),
|
|
6601
6652
|
finishedAt: exports_external.number().int().nonnegative().nullable(),
|
|
6653
|
+
currentCommand: exports_external.string().nullable().optional(),
|
|
6602
6654
|
steps: exports_external.array(runStepSchema).optional()
|
|
6603
6655
|
});
|
|
6604
6656
|
var runSchema = exports_external.object({
|
|
@@ -6817,758 +6869,940 @@ function pickLatestRun(runs) {
|
|
|
6817
6869
|
return null;
|
|
6818
6870
|
return runs.reduce((best, run) => run.createdAt > best.createdAt ? run : best);
|
|
6819
6871
|
}
|
|
6820
|
-
// ../runner-client/src/
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6872
|
+
// ../runner-client/src/xml-entities.ts
|
|
6873
|
+
var NAMED = {
|
|
6874
|
+
amp: "&",
|
|
6875
|
+
lt: "<",
|
|
6876
|
+
gt: ">",
|
|
6877
|
+
quot: '"',
|
|
6878
|
+
apos: "'"
|
|
6879
|
+
};
|
|
6880
|
+
function decodeXmlEntities(value) {
|
|
6881
|
+
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]+);/g, (entity, body) => {
|
|
6882
|
+
if (body.startsWith("#")) {
|
|
6883
|
+
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
6884
|
+
if (!Number.isFinite(code) || code < 0 || code > 1114111) {
|
|
6885
|
+
return entity;
|
|
6886
|
+
}
|
|
6887
|
+
try {
|
|
6888
|
+
return String.fromCodePoint(code);
|
|
6889
|
+
} catch {
|
|
6890
|
+
return entity;
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
return NAMED[body] ?? entity;
|
|
6894
|
+
});
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6897
|
+
// ../runner-client/src/shell-script.ts
|
|
6898
|
+
function shellSingleQuote(value) {
|
|
6899
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6900
|
+
}
|
|
6901
|
+
function tokenizeShellLine(line) {
|
|
6902
|
+
const tokens = [];
|
|
6903
|
+
let current = "";
|
|
6904
|
+
let quote = null;
|
|
6905
|
+
for (let i = 0;i < line.length; i++) {
|
|
6906
|
+
const ch = line[i];
|
|
6907
|
+
if (ch == null)
|
|
6908
|
+
continue;
|
|
6909
|
+
if (quote === "'") {
|
|
6910
|
+
if (ch === "'") {
|
|
6911
|
+
quote = null;
|
|
6912
|
+
} else {
|
|
6913
|
+
current += ch;
|
|
6914
|
+
}
|
|
6915
|
+
continue;
|
|
6916
|
+
}
|
|
6917
|
+
if (quote === '"') {
|
|
6918
|
+
if (ch === "\\") {
|
|
6919
|
+
const next = line[i + 1];
|
|
6920
|
+
if (next != null) {
|
|
6921
|
+
current += next;
|
|
6922
|
+
i++;
|
|
6923
|
+
}
|
|
6924
|
+
continue;
|
|
6925
|
+
}
|
|
6926
|
+
if (ch === '"') {
|
|
6927
|
+
quote = null;
|
|
6928
|
+
} else {
|
|
6929
|
+
current += ch;
|
|
6930
|
+
}
|
|
6931
|
+
continue;
|
|
6932
|
+
}
|
|
6933
|
+
if (ch === "'" || ch === '"') {
|
|
6934
|
+
quote = ch;
|
|
6935
|
+
continue;
|
|
6936
|
+
}
|
|
6937
|
+
if (/\s/.test(ch)) {
|
|
6938
|
+
if (current.length > 0) {
|
|
6939
|
+
tokens.push(current);
|
|
6940
|
+
current = "";
|
|
6941
|
+
}
|
|
6942
|
+
continue;
|
|
6943
|
+
}
|
|
6944
|
+
current += ch;
|
|
6839
6945
|
}
|
|
6840
|
-
if (
|
|
6841
|
-
|
|
6842
|
-
const text = typeof record.text === "string" ? record.text : "";
|
|
6843
|
-
return text ? `Assert ${assertion}: ${text}` : `Assert ${assertion}`;
|
|
6946
|
+
if (quote != null) {
|
|
6947
|
+
throw new Error("Unclosed quote");
|
|
6844
6948
|
}
|
|
6845
|
-
if (
|
|
6846
|
-
|
|
6847
|
-
if (type === "done")
|
|
6848
|
-
return "Done";
|
|
6849
|
-
if (type === "fail")
|
|
6850
|
-
return "Failed";
|
|
6851
|
-
if (type === "swipe")
|
|
6852
|
-
return "Swipe";
|
|
6853
|
-
if (type === "input") {
|
|
6854
|
-
return `Input${typeof record.text === "string" ? `: ${record.text}` : ""}`;
|
|
6949
|
+
if (current.length > 0) {
|
|
6950
|
+
tokens.push(current);
|
|
6855
6951
|
}
|
|
6856
|
-
return
|
|
6857
|
-
}
|
|
6858
|
-
function stepReasoning(step) {
|
|
6859
|
-
const action = step.action && typeof step.action === "object" ? step.action : null;
|
|
6860
|
-
const reasonFromAction = typeof action?.reason === "string" && action.reason.trim() ? action.reason.trim() : null;
|
|
6861
|
-
const thoughtsFromAction = typeof action?.thoughts === "string" && action.thoughts.trim() ? action.thoughts.trim() : null;
|
|
6862
|
-
const reason = reasonFromAction ?? (step.detail?.trim() ? step.detail.trim() : null);
|
|
6863
|
-
const thoughts = thoughtsFromAction && thoughtsFromAction !== reason ? thoughtsFromAction : null;
|
|
6864
|
-
return { reason, thoughts };
|
|
6865
|
-
}
|
|
6866
|
-
function toReportStatus(status) {
|
|
6867
|
-
if (status === "passed" || status === "errored" || status === "cancelled")
|
|
6868
|
-
return status;
|
|
6869
|
-
return null;
|
|
6870
|
-
}
|
|
6871
|
-
function mapCatalogStep(step, screenshotsByStepId) {
|
|
6872
|
-
const { reason, thoughts } = stepReasoning(step);
|
|
6873
|
-
return {
|
|
6874
|
-
id: step.id,
|
|
6875
|
-
index: step.idx + 1,
|
|
6876
|
-
summary: actionSummary(step.action),
|
|
6877
|
-
ok: step.ok,
|
|
6878
|
-
latencyMs: step.latencyMs,
|
|
6879
|
-
detail: step.detail,
|
|
6880
|
-
reason,
|
|
6881
|
-
thoughts,
|
|
6882
|
-
screenshotBase64: screenshotsByStepId[step.id] ?? null
|
|
6883
|
-
};
|
|
6952
|
+
return tokens;
|
|
6884
6953
|
}
|
|
6885
|
-
function
|
|
6886
|
-
const
|
|
6887
|
-
|
|
6888
|
-
|
|
6954
|
+
function parseFlagMap(tokens) {
|
|
6955
|
+
const flags = new Map;
|
|
6956
|
+
for (let i = 0;i < tokens.length; i++) {
|
|
6957
|
+
const token = tokens[i];
|
|
6958
|
+
if (token == null || !token.startsWith("-"))
|
|
6959
|
+
continue;
|
|
6960
|
+
const eq = token.indexOf("=");
|
|
6961
|
+
if (eq > 0) {
|
|
6962
|
+
flags.set(token.slice(0, eq), token.slice(eq + 1));
|
|
6963
|
+
continue;
|
|
6964
|
+
}
|
|
6965
|
+
const next = tokens[i + 1];
|
|
6966
|
+
if (next != null && !next.startsWith("-")) {
|
|
6967
|
+
flags.set(token, next);
|
|
6968
|
+
i++;
|
|
6969
|
+
} else {
|
|
6970
|
+
flags.set(token, true);
|
|
6971
|
+
}
|
|
6889
6972
|
}
|
|
6890
|
-
|
|
6891
|
-
const tests = run.tests.map((test) => ({
|
|
6892
|
-
id: test.id,
|
|
6893
|
-
title: caseTitles[test.caseId] ?? test.caseId,
|
|
6894
|
-
status: test.status,
|
|
6895
|
-
executionMode: test.executionMode ?? null,
|
|
6896
|
-
error: test.error,
|
|
6897
|
-
startedAt: test.startedAt,
|
|
6898
|
-
finishedAt: test.finishedAt,
|
|
6899
|
-
steps: (test.steps ?? []).map((step) => mapCatalogStep(step, screenshotsByStepId))
|
|
6900
|
-
}));
|
|
6901
|
-
const primaryTitle = tests[0]?.title ?? "Run";
|
|
6902
|
-
return {
|
|
6903
|
-
id: run.id,
|
|
6904
|
-
source: "catalog",
|
|
6905
|
-
status,
|
|
6906
|
-
title: primaryTitle,
|
|
6907
|
-
appLabel: meta.appLabel ?? null,
|
|
6908
|
-
deviceLabel: meta.deviceLabel ?? null,
|
|
6909
|
-
platform: run.platform,
|
|
6910
|
-
executionMode: run.executionMode,
|
|
6911
|
-
error: run.error,
|
|
6912
|
-
createdAt: run.createdAt,
|
|
6913
|
-
startedAt: run.startedAt,
|
|
6914
|
-
finishedAt: run.finishedAt,
|
|
6915
|
-
tests
|
|
6916
|
-
};
|
|
6917
|
-
}
|
|
6918
|
-
function suggestedRunReportBasename(doc) {
|
|
6919
|
-
const shortId = doc.id.replace(/[^a-zA-Z0-9]+/g, "").slice(0, 8) || "run";
|
|
6920
|
-
const raw = ["yoqa-run", shortId, doc.status].join("-").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
6921
|
-
return raw || "yoqa-run-report";
|
|
6922
|
-
}
|
|
6923
|
-
function escapeHtml(value) {
|
|
6924
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6973
|
+
return flags;
|
|
6925
6974
|
}
|
|
6926
|
-
function
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6975
|
+
function flagString(flags, ...names) {
|
|
6976
|
+
for (const name of names) {
|
|
6977
|
+
const value = flags.get(name);
|
|
6978
|
+
if (typeof value === "string")
|
|
6979
|
+
return value;
|
|
6980
|
+
}
|
|
6981
|
+
return;
|
|
6930
6982
|
}
|
|
6931
|
-
function
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
const seconds = ms / 1000;
|
|
6938
|
-
if (seconds < 60)
|
|
6939
|
-
return `${seconds.toFixed(1)}s`;
|
|
6940
|
-
const minutes = Math.floor(seconds / 60);
|
|
6941
|
-
const rem = Math.round(seconds % 60);
|
|
6942
|
-
return `${minutes}m ${rem}s`;
|
|
6983
|
+
function flagNumber(flags, ...names) {
|
|
6984
|
+
const raw = flagString(flags, ...names);
|
|
6985
|
+
if (raw == null)
|
|
6986
|
+
return;
|
|
6987
|
+
const n = Number(raw);
|
|
6988
|
+
return Number.isFinite(n) ? n : undefined;
|
|
6943
6989
|
}
|
|
6944
|
-
function
|
|
6945
|
-
if (
|
|
6946
|
-
|
|
6990
|
+
function parseActionFromTokens(tokens, lineNumber, raw) {
|
|
6991
|
+
if (tokens[0] !== "yoqa" || tokens[1] !== "action") {
|
|
6992
|
+
throw new Error("Expected `yoqa action <kind> …`");
|
|
6947
6993
|
}
|
|
6948
|
-
|
|
6949
|
-
|
|
6994
|
+
const kindRaw = tokens[2];
|
|
6995
|
+
if (!kindRaw) {
|
|
6996
|
+
throw new Error("Missing action kind");
|
|
6950
6997
|
}
|
|
6951
|
-
|
|
6998
|
+
const kindParsed = actionKindSchema.safeParse(kindRaw);
|
|
6999
|
+
if (!kindParsed.success) {
|
|
7000
|
+
throw new Error(`Unknown action kind: ${kindRaw}`);
|
|
7001
|
+
}
|
|
7002
|
+
const kind = kindParsed.data;
|
|
7003
|
+
const flags = parseFlagMap(tokens.slice(3));
|
|
7004
|
+
const action = { kind };
|
|
7005
|
+
const description = flagString(flags, "-d", "--description");
|
|
7006
|
+
if (description != null)
|
|
7007
|
+
action.description = description;
|
|
7008
|
+
const label = flagString(flags, "--label");
|
|
7009
|
+
if (label != null)
|
|
7010
|
+
action.label = label;
|
|
7011
|
+
const id = flagString(flags, "--id");
|
|
7012
|
+
if (id != null)
|
|
7013
|
+
action.id = id;
|
|
7014
|
+
const x = flagNumber(flags, "--x");
|
|
7015
|
+
const y = flagNumber(flags, "--y");
|
|
7016
|
+
const x2 = flagNumber(flags, "--x2");
|
|
7017
|
+
const y2 = flagNumber(flags, "--y2");
|
|
7018
|
+
const durationMs = flagNumber(flags, "--duration");
|
|
7019
|
+
const text = flagString(flags, "--text");
|
|
7020
|
+
const appId = flagString(flags, "--app-id", "--bundle-id");
|
|
7021
|
+
const url = flagString(flags, "--url");
|
|
7022
|
+
const seconds = flagNumber(flags, "--seconds");
|
|
7023
|
+
if (x != null)
|
|
7024
|
+
action.x = x;
|
|
7025
|
+
if (y != null)
|
|
7026
|
+
action.y = y;
|
|
7027
|
+
if (x2 != null)
|
|
7028
|
+
action.x2 = x2;
|
|
7029
|
+
if (y2 != null)
|
|
7030
|
+
action.y2 = y2;
|
|
7031
|
+
if (durationMs != null)
|
|
7032
|
+
action.durationMs = durationMs;
|
|
7033
|
+
if (text != null)
|
|
7034
|
+
action.text = text;
|
|
7035
|
+
if (appId != null)
|
|
7036
|
+
action.appId = appId;
|
|
7037
|
+
if (url != null)
|
|
7038
|
+
action.url = url;
|
|
7039
|
+
if (seconds != null)
|
|
7040
|
+
action.seconds = seconds;
|
|
7041
|
+
if (flags.has("--double"))
|
|
7042
|
+
action.double = true;
|
|
7043
|
+
if (flags.has("--dismiss")) {
|
|
7044
|
+
action.alertAction = "dismiss";
|
|
7045
|
+
} else if (flags.has("--accept") || kind === "alert") {
|
|
7046
|
+
const alertAction = flagString(flags, "--action");
|
|
7047
|
+
if (alertAction === "dismiss")
|
|
7048
|
+
action.alertAction = "dismiss";
|
|
7049
|
+
else if (alertAction === "accept" || kind === "alert")
|
|
7050
|
+
action.alertAction = "accept";
|
|
7051
|
+
}
|
|
7052
|
+
return { kind: "action", action, lineNumber, raw };
|
|
6952
7053
|
}
|
|
6953
|
-
function
|
|
6954
|
-
|
|
7054
|
+
function parseScreenshotFromTokens(tokens, lineNumber, raw) {
|
|
7055
|
+
if (tokens[0] !== "yoqa" || tokens[1] !== "screenshot") {
|
|
7056
|
+
throw new Error("Expected `yoqa screenshot [path]`");
|
|
7057
|
+
}
|
|
7058
|
+
const pathToken = tokens[2];
|
|
7059
|
+
if (pathToken?.startsWith("-")) {
|
|
7060
|
+
throw new Error("`yoqa screenshot` accepts an optional output path only");
|
|
7061
|
+
}
|
|
7062
|
+
if (tokens.length > 3) {
|
|
7063
|
+
throw new Error("`yoqa screenshot` accepts at most one path argument");
|
|
7064
|
+
}
|
|
7065
|
+
const path = pathToken?.trim() ? pathToken.trim() : null;
|
|
7066
|
+
return { kind: "screenshot", path, lineNumber, raw };
|
|
6955
7067
|
}
|
|
6956
|
-
function
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
["Status", accent.label],
|
|
6960
|
-
["Source", doc.source === "catalog" ? "Catalog run" : "Manual Inspector"],
|
|
6961
|
-
["App", doc.appLabel ?? "—"],
|
|
6962
|
-
["Device", doc.deviceLabel ?? "—"],
|
|
6963
|
-
["Platform", doc.platform ?? "—"],
|
|
6964
|
-
["Mode", doc.executionMode ?? "—"],
|
|
6965
|
-
["Started", formatWhen(doc.startedAt ?? doc.createdAt)],
|
|
6966
|
-
["Finished", formatWhen(doc.finishedAt)],
|
|
6967
|
-
["Duration", formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt)],
|
|
6968
|
-
["Run ID", doc.id]
|
|
6969
|
-
];
|
|
6970
|
-
const testsHtml = doc.tests.map((test, testIndex) => {
|
|
6971
|
-
const testAccent = statusAccent(test.status === "passed" || test.status === "errored" || test.status === "cancelled" ? test.status : doc.status);
|
|
6972
|
-
const stepsHtml = test.steps.map((step) => {
|
|
6973
|
-
const stepStatus = step.ok ? "Passed" : "Failed";
|
|
6974
|
-
const stepColor = step.ok ? "#166534" : "#991b1b";
|
|
6975
|
-
const reasonBlock = step.reason ? `<p class="reason">${escapeHtml(step.reason)}</p>` : "";
|
|
6976
|
-
const thoughtsBlock = step.thoughts ? `<details class="thoughts"><summary>AI thoughts</summary><p>${escapeHtml(step.thoughts)}</p></details>` : "";
|
|
6977
|
-
const detailBlock = step.detail && step.detail !== step.reason ? `<p class="detail">${escapeHtml(step.detail)}</p>` : "";
|
|
6978
|
-
const shot = step.screenshotBase64 ? `<img class="shot" alt="Step ${step.index} screenshot" src="${pngDataUri(step.screenshotBase64)}" />` : `<p class="muted">No screenshot</p>`;
|
|
6979
|
-
const latency = step.latencyMs != null ? `<span class="muted">${step.latencyMs}ms</span>` : "";
|
|
6980
|
-
return `
|
|
6981
|
-
<article class="step ${step.ok ? "ok" : "fail"}">
|
|
6982
|
-
<header>
|
|
6983
|
-
<span class="idx">${step.index}</span>
|
|
6984
|
-
<div>
|
|
6985
|
-
<h3>${escapeHtml(step.summary)}</h3>
|
|
6986
|
-
<p class="step-status" style="color:${stepColor}">${stepStatus} ${latency}</p>
|
|
6987
|
-
</div>
|
|
6988
|
-
</header>
|
|
6989
|
-
${reasonBlock}
|
|
6990
|
-
${detailBlock}
|
|
6991
|
-
${thoughtsBlock}
|
|
6992
|
-
${shot}
|
|
6993
|
-
</article>`;
|
|
6994
|
-
}).join(`
|
|
6995
|
-
`);
|
|
6996
|
-
return `
|
|
6997
|
-
<section class="test">
|
|
6998
|
-
<h2>
|
|
6999
|
-
<span class="test-num">${testIndex + 1}</span>
|
|
7000
|
-
${escapeHtml(test.title)}
|
|
7001
|
-
<span class="pill" style="background:${testAccent.bg};color:${testAccent.fg}">${escapeHtml(testAccent.label)}</span>
|
|
7002
|
-
</h2>
|
|
7003
|
-
${test.error ? `<p class="error">${escapeHtml(test.error)}</p>` : ""}
|
|
7004
|
-
${test.executionMode ? `<p class="muted">Mode: ${escapeHtml(test.executionMode)}</p>` : ""}
|
|
7005
|
-
<div class="steps">${stepsHtml || `<p class="muted">No steps recorded</p>`}</div>
|
|
7006
|
-
</section>`;
|
|
7007
|
-
}).join(`
|
|
7008
|
-
`);
|
|
7009
|
-
const metaHtml = metaRows.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`).join(`
|
|
7010
|
-
`);
|
|
7011
|
-
return `<!DOCTYPE html>
|
|
7012
|
-
<html lang="en">
|
|
7013
|
-
<head>
|
|
7014
|
-
<meta charset="utf-8" />
|
|
7015
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7016
|
-
<title>Yoqa Report — ${escapeHtml(doc.title)} (${escapeHtml(accent.label)})</title>
|
|
7017
|
-
<style>
|
|
7018
|
-
:root { color-scheme: light; }
|
|
7019
|
-
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #f8fafc; color: #0f172a; line-height: 1.5; }
|
|
7020
|
-
.wrap { max-width: 52rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
7021
|
-
.banner { border-radius: 1rem; padding: 1.25rem 1.5rem; background: ${accent.bg}; color: ${accent.fg}; margin-bottom: 1.5rem; }
|
|
7022
|
-
.banner h1 { margin: 0 0 0.35rem; font-size: 1.5rem; }
|
|
7023
|
-
.banner p { margin: 0; opacity: 0.9; }
|
|
7024
|
-
table.meta { width: 100%; border-collapse: collapse; background: #fff; border-radius: 0.75rem; overflow: hidden; box-shadow: 0 1px 2px rgb(0 0 0 / 0.06); margin-bottom: 2rem; }
|
|
7025
|
-
table.meta th, table.meta td { text-align: left; padding: 0.65rem 1rem; border-bottom: 1px solid #e2e8f0; font-size: 0.9rem; }
|
|
7026
|
-
table.meta th { width: 8rem; color: #64748b; font-weight: 600; }
|
|
7027
|
-
.error { background: #fee2e2; color: #991b1b; padding: 0.75rem 1rem; border-radius: 0.5rem; }
|
|
7028
|
-
.test { margin-bottom: 2.5rem; }
|
|
7029
|
-
.test h2 { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 1.15rem; }
|
|
7030
|
-
.test-num { display: inline-flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; border-radius: 999px; background: #0f172a; color: #fff; font-size: 0.8rem; }
|
|
7031
|
-
.pill { display: inline-flex; align-items: center; border-radius: 999px; padding: 0.15rem 0.65rem; font-size: 0.75rem; font-weight: 600; }
|
|
7032
|
-
.step { background: #fff; border: 1px solid #e2e8f0; border-radius: 0.75rem; padding: 1rem; margin: 0.75rem 0; }
|
|
7033
|
-
.step.fail { border-color: #fecaca; }
|
|
7034
|
-
.step header { display: flex; gap: 0.75rem; align-items: flex-start; }
|
|
7035
|
-
.step .idx { flex-shrink: 0; width: 1.75rem; height: 1.75rem; border-radius: 999px; background: #e2e8f0; display: inline-flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; }
|
|
7036
|
-
.step.ok .idx { background: #bbf7d0; color: #166534; }
|
|
7037
|
-
.step.fail .idx { background: #fecaca; color: #991b1b; }
|
|
7038
|
-
.step h3 { margin: 0; font-size: 1rem; }
|
|
7039
|
-
.step-status { margin: 0.15rem 0 0; font-size: 0.8rem; font-weight: 600; }
|
|
7040
|
-
.reason, .detail { margin: 0.5rem 0 0; color: #475569; font-size: 0.9rem; }
|
|
7041
|
-
.thoughts { margin-top: 0.5rem; font-size: 0.85rem; color: #64748b; }
|
|
7042
|
-
.thoughts p { margin: 0.35rem 0 0; white-space: pre-wrap; }
|
|
7043
|
-
.shot { display: block; max-width: 100%; width: min(22rem, 100%); margin-top: 0.75rem; border-radius: 0.75rem; border: 1px solid #e2e8f0; }
|
|
7044
|
-
.muted { color: #94a3b8; font-size: 0.85rem; }
|
|
7045
|
-
footer { margin-top: 2rem; color: #94a3b8; font-size: 0.8rem; }
|
|
7046
|
-
@media print {
|
|
7047
|
-
body { background: #fff; }
|
|
7048
|
-
.shot { max-height: 60vh; object-fit: contain; }
|
|
7068
|
+
function parseAssertFromTokens(tokens, lineNumber, raw) {
|
|
7069
|
+
if (tokens[0] !== "yoqa" || tokens[1] !== "assert") {
|
|
7070
|
+
throw new Error("Expected `yoqa assert <visible|not-visible> …`");
|
|
7049
7071
|
}
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7072
|
+
const assertionRaw = tokens[2];
|
|
7073
|
+
if (assertionRaw !== "visible" && assertionRaw !== "not-visible") {
|
|
7074
|
+
throw new Error("Assert kind must be `visible` or `not-visible`");
|
|
7075
|
+
}
|
|
7076
|
+
const flags = parseFlagMap(tokens.slice(3));
|
|
7077
|
+
const text = flagString(flags, "--text", "-t");
|
|
7078
|
+
if (!text || text.trim().length === 0) {
|
|
7079
|
+
throw new Error("assert requires --text");
|
|
7080
|
+
}
|
|
7081
|
+
const timeout = flagNumber(flags, "--timeout") ?? 5;
|
|
7082
|
+
if (timeout < 0) {
|
|
7083
|
+
throw new Error("--timeout must be non-negative");
|
|
7084
|
+
}
|
|
7085
|
+
return {
|
|
7086
|
+
kind: "assert",
|
|
7087
|
+
assertion: assertionRaw,
|
|
7088
|
+
text: text.trim(),
|
|
7089
|
+
timeoutSeconds: timeout,
|
|
7090
|
+
lineNumber,
|
|
7091
|
+
raw
|
|
7092
|
+
};
|
|
7066
7093
|
}
|
|
7067
|
-
function
|
|
7068
|
-
|
|
7094
|
+
function isIgnorableLine(trimmed) {
|
|
7095
|
+
if (trimmed.length === 0)
|
|
7096
|
+
return true;
|
|
7097
|
+
if (trimmed.startsWith("#"))
|
|
7098
|
+
return true;
|
|
7099
|
+
if (trimmed === "set -euo pipefail" || trimmed === "set -e" || trimmed === "set -eu")
|
|
7100
|
+
return true;
|
|
7101
|
+
return false;
|
|
7069
7102
|
}
|
|
7070
|
-
function
|
|
7071
|
-
const
|
|
7072
|
-
const
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
""
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
lines.push(`> **Error:** ${doc.error}`, "");
|
|
7092
|
-
}
|
|
7093
|
-
doc.tests.forEach((test, testIndex) => {
|
|
7094
|
-
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
7095
|
-
lines.push(`## ${testIndex + 1}. ${test.title} (${testLabel})`, "");
|
|
7096
|
-
if (test.executionMode)
|
|
7097
|
-
lines.push(`Mode: \`${test.executionMode}\``, "");
|
|
7098
|
-
if (test.error)
|
|
7099
|
-
lines.push(`**Error:** ${test.error}`, "");
|
|
7100
|
-
if (test.steps.length === 0) {
|
|
7101
|
-
lines.push("_No steps recorded_", "");
|
|
7102
|
-
return;
|
|
7103
|
-
}
|
|
7104
|
-
for (const step of test.steps) {
|
|
7105
|
-
lines.push(`### Step ${step.index}: ${step.summary}`);
|
|
7106
|
-
lines.push("");
|
|
7107
|
-
lines.push(`- Result: **${step.ok ? "Passed" : "Failed"}**`);
|
|
7108
|
-
if (step.latencyMs != null)
|
|
7109
|
-
lines.push(`- Latency: ${step.latencyMs}ms`);
|
|
7110
|
-
if (step.reason)
|
|
7111
|
-
lines.push(`- Reason: ${step.reason}`);
|
|
7112
|
-
if (step.detail && step.detail !== step.reason)
|
|
7113
|
-
lines.push(`- Detail: ${step.detail}`);
|
|
7114
|
-
if (step.thoughts) {
|
|
7115
|
-
lines.push("", "<details><summary>AI thoughts</summary>", "", step.thoughts, "", "</details>");
|
|
7103
|
+
function parseYoqaShellScript(text) {
|
|
7104
|
+
const lines = text.split(/\r?\n/);
|
|
7105
|
+
const steps = [];
|
|
7106
|
+
const errors2 = [];
|
|
7107
|
+
for (let i = 0;i < lines.length; i++) {
|
|
7108
|
+
const lineNumber = i + 1;
|
|
7109
|
+
const raw = lines[i] ?? "";
|
|
7110
|
+
const trimmed = raw.trim();
|
|
7111
|
+
if (isIgnorableLine(trimmed))
|
|
7112
|
+
continue;
|
|
7113
|
+
try {
|
|
7114
|
+
const tokens = tokenizeShellLine(trimmed);
|
|
7115
|
+
if (tokens.length === 0)
|
|
7116
|
+
continue;
|
|
7117
|
+
if (tokens[0] === "sleep") {
|
|
7118
|
+
const seconds = Number(tokens[1]);
|
|
7119
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
7120
|
+
throw new Error("`sleep` requires a non-negative number of seconds");
|
|
7121
|
+
}
|
|
7122
|
+
steps.push({ kind: "sleep", seconds, lineNumber, raw: trimmed });
|
|
7123
|
+
continue;
|
|
7116
7124
|
}
|
|
7117
|
-
if (
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
lines.push("", "_No screenshot_");
|
|
7125
|
+
if (tokens[0] === "yoqa" && tokens[1] === "assert") {
|
|
7126
|
+
steps.push(parseAssertFromTokens(tokens, lineNumber, trimmed));
|
|
7127
|
+
continue;
|
|
7121
7128
|
}
|
|
7122
|
-
|
|
7129
|
+
if (tokens[0] === "yoqa" && tokens[1] === "screenshot") {
|
|
7130
|
+
steps.push(parseScreenshotFromTokens(tokens, lineNumber, trimmed));
|
|
7131
|
+
continue;
|
|
7132
|
+
}
|
|
7133
|
+
if (tokens[0] === "yoqa") {
|
|
7134
|
+
steps.push(parseActionFromTokens(tokens, lineNumber, trimmed));
|
|
7135
|
+
continue;
|
|
7136
|
+
}
|
|
7137
|
+
throw new Error(`Unsupported command: ${tokens[0]}`);
|
|
7138
|
+
} catch (error) {
|
|
7139
|
+
errors2.push({
|
|
7140
|
+
lineNumber,
|
|
7141
|
+
raw: trimmed,
|
|
7142
|
+
message: error instanceof Error ? error.message : String(error)
|
|
7143
|
+
});
|
|
7123
7144
|
}
|
|
7124
|
-
}
|
|
7125
|
-
|
|
7126
|
-
return lines.join(`
|
|
7127
|
-
`);
|
|
7145
|
+
}
|
|
7146
|
+
return { steps, errors: errors2 };
|
|
7128
7147
|
}
|
|
7129
|
-
function
|
|
7130
|
-
const
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
"
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
if (
|
|
7151
|
-
|
|
7148
|
+
function formatActionShellLine(action) {
|
|
7149
|
+
const parts = ["yoqa", "action", action.kind];
|
|
7150
|
+
if (action.id) {
|
|
7151
|
+
parts.push("--id", shellSingleQuote(action.id));
|
|
7152
|
+
}
|
|
7153
|
+
if (action.label) {
|
|
7154
|
+
parts.push("--label", shellSingleQuote(action.label));
|
|
7155
|
+
}
|
|
7156
|
+
if (action.description) {
|
|
7157
|
+
parts.push("-d", shellSingleQuote(action.description));
|
|
7158
|
+
}
|
|
7159
|
+
if (action.x != null)
|
|
7160
|
+
parts.push("--x", String(Math.round(action.x)));
|
|
7161
|
+
if (action.y != null)
|
|
7162
|
+
parts.push("--y", String(Math.round(action.y)));
|
|
7163
|
+
if (action.x2 != null)
|
|
7164
|
+
parts.push("--x2", String(Math.round(action.x2)));
|
|
7165
|
+
if (action.y2 != null)
|
|
7166
|
+
parts.push("--y2", String(Math.round(action.y2)));
|
|
7167
|
+
if (action.durationMs != null)
|
|
7168
|
+
parts.push("--duration", String(Math.round(action.durationMs)));
|
|
7169
|
+
if (action.double)
|
|
7170
|
+
parts.push("--double");
|
|
7171
|
+
if (action.text != null)
|
|
7172
|
+
parts.push("--text", shellSingleQuote(action.text));
|
|
7173
|
+
if (action.appId != null)
|
|
7174
|
+
parts.push("--app-id", shellSingleQuote(action.appId));
|
|
7175
|
+
if (action.url != null)
|
|
7176
|
+
parts.push("--url", shellSingleQuote(action.url));
|
|
7177
|
+
if (action.seconds != null)
|
|
7178
|
+
parts.push("--seconds", String(action.seconds));
|
|
7179
|
+
if (action.kind === "alert") {
|
|
7180
|
+
if (action.alertAction === "dismiss")
|
|
7181
|
+
parts.push("--dismiss");
|
|
7152
7182
|
}
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7183
|
+
return parts.join(" ");
|
|
7184
|
+
}
|
|
7185
|
+
function formatSleepShellLine(seconds) {
|
|
7186
|
+
const safe = Math.max(0, seconds);
|
|
7187
|
+
const rounded = Number.isInteger(safe) ? String(safe) : String(Number(safe.toFixed(3)));
|
|
7188
|
+
return `sleep ${rounded}`;
|
|
7189
|
+
}
|
|
7190
|
+
function formatAssertShellLine(input) {
|
|
7191
|
+
const parts = ["yoqa", "assert", input.assertion, "--text", shellSingleQuote(input.text.trim())];
|
|
7192
|
+
const timeout = input.timeoutSeconds ?? 5;
|
|
7193
|
+
if (timeout !== 5) {
|
|
7194
|
+
parts.push("--timeout", String(timeout));
|
|
7160
7195
|
}
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7196
|
+
return parts.join(" ");
|
|
7197
|
+
}
|
|
7198
|
+
var DEFAULT_SHELL_SCRIPT_HEADER = [
|
|
7199
|
+
"#!/usr/bin/env bash",
|
|
7200
|
+
"# Yoqa inspector script",
|
|
7201
|
+
"# Requires an active device session: yoqa devices connect <id>",
|
|
7202
|
+
"set -euo pipefail",
|
|
7203
|
+
""
|
|
7204
|
+
].join(`
|
|
7205
|
+
`);
|
|
7206
|
+
function sleepMs(ms, signal) {
|
|
7207
|
+
if (ms <= 0)
|
|
7208
|
+
return Promise.resolve();
|
|
7209
|
+
return new Promise((resolve, reject) => {
|
|
7210
|
+
if (signal?.aborted) {
|
|
7211
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
7212
|
+
return;
|
|
7170
7213
|
}
|
|
7171
|
-
|
|
7214
|
+
const timer = setTimeout(() => {
|
|
7215
|
+
signal?.removeEventListener("abort", onAbort);
|
|
7216
|
+
resolve();
|
|
7217
|
+
}, ms);
|
|
7218
|
+
const onAbort = () => {
|
|
7219
|
+
clearTimeout(timer);
|
|
7220
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
7221
|
+
};
|
|
7222
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7223
|
+
});
|
|
7224
|
+
}
|
|
7225
|
+
function normalizedLabel(value) {
|
|
7226
|
+
return decodeXmlEntities(value ?? "").trim().toLowerCase();
|
|
7227
|
+
}
|
|
7228
|
+
function screenHasText(elements, text) {
|
|
7229
|
+
const needle = normalizedLabel(text);
|
|
7230
|
+
if (!needle)
|
|
7231
|
+
return false;
|
|
7232
|
+
return (elements ?? []).some((el) => {
|
|
7233
|
+
const label = normalizedLabel(el.label);
|
|
7234
|
+
const type = el.type?.toLowerCase() ?? "";
|
|
7235
|
+
return label.includes(needle) || type.includes(needle);
|
|
7236
|
+
});
|
|
7237
|
+
}
|
|
7238
|
+
async function runAssertStep(client, step, signal) {
|
|
7239
|
+
const deadline = Date.now() + step.timeoutSeconds * 1000;
|
|
7240
|
+
for (;; ) {
|
|
7241
|
+
if (signal?.aborted) {
|
|
7242
|
+
throw Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
7243
|
+
}
|
|
7244
|
+
const screen = await client.getScreen();
|
|
7245
|
+
const found = screenHasText(screen.elements, step.text);
|
|
7246
|
+
if (step.assertion === "visible" && found)
|
|
7247
|
+
return;
|
|
7248
|
+
if (step.assertion === "not-visible" && !found)
|
|
7249
|
+
return;
|
|
7250
|
+
if (Date.now() >= deadline) {
|
|
7251
|
+
throw new Error(step.assertion === "visible" ? `Expected visible text not found within ${step.timeoutSeconds}s: ${step.text}` : `Unexpected text still visible after ${step.timeoutSeconds}s: ${step.text}`);
|
|
7252
|
+
}
|
|
7253
|
+
await sleepMs(400, signal);
|
|
7172
7254
|
}
|
|
7173
|
-
lines.push("Download the `yoqa-report` artifact for the full HTML report with screenshots.", "");
|
|
7174
|
-
return lines.join(`
|
|
7175
|
-
`);
|
|
7176
7255
|
}
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7256
|
+
async function runYoqaShellScript(client, text, options = {}) {
|
|
7257
|
+
const parsed = parseYoqaShellScript(text);
|
|
7258
|
+
if (parsed.errors.length > 0) {
|
|
7259
|
+
const first = parsed.errors[0];
|
|
7260
|
+
if (!first) {
|
|
7261
|
+
return { ok: false, completed: 0, total: parsed.steps.length, error: "Parse error" };
|
|
7262
|
+
}
|
|
7263
|
+
return {
|
|
7264
|
+
ok: false,
|
|
7265
|
+
completed: 0,
|
|
7266
|
+
total: parsed.steps.length,
|
|
7267
|
+
error: `Line ${first.lineNumber}: ${first.message}`
|
|
7268
|
+
};
|
|
7269
|
+
}
|
|
7270
|
+
const { steps } = parsed;
|
|
7271
|
+
const total = steps.length;
|
|
7272
|
+
if (total === 0) {
|
|
7273
|
+
return { ok: true, completed: 0, total: 0 };
|
|
7274
|
+
}
|
|
7275
|
+
for (let index = 0;index < steps.length; index++) {
|
|
7276
|
+
if (options.signal?.aborted) {
|
|
7277
|
+
return { ok: false, completed: index, total, error: "Aborted" };
|
|
7197
7278
|
}
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
}
|
|
7201
|
-
|
|
7202
|
-
// ../runner-client/src/shell-script.ts
|
|
7203
|
-
function shellSingleQuote(value) {
|
|
7204
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7205
|
-
}
|
|
7206
|
-
function tokenizeShellLine(line) {
|
|
7207
|
-
const tokens = [];
|
|
7208
|
-
let current = "";
|
|
7209
|
-
let quote = null;
|
|
7210
|
-
for (let i = 0;i < line.length; i++) {
|
|
7211
|
-
const ch = line[i];
|
|
7212
|
-
if (ch == null)
|
|
7279
|
+
const step = steps[index];
|
|
7280
|
+
if (!step)
|
|
7213
7281
|
continue;
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7282
|
+
await options.onStep?.({ index, total, step, status: "running" });
|
|
7283
|
+
try {
|
|
7284
|
+
if (step.kind === "sleep") {
|
|
7285
|
+
await sleepMs(step.seconds * 1000, options.signal);
|
|
7286
|
+
} else if (step.kind === "assert") {
|
|
7287
|
+
await runAssertStep(client, step, options.signal);
|
|
7288
|
+
} else if (step.kind === "screenshot") {
|
|
7289
|
+
await client.takeScreenshot(step.path ? { path: step.path } : {});
|
|
7217
7290
|
} else {
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
}
|
|
7222
|
-
if (quote === '"') {
|
|
7223
|
-
if (ch === "\\") {
|
|
7224
|
-
const next = line[i + 1];
|
|
7225
|
-
if (next != null) {
|
|
7226
|
-
current += next;
|
|
7227
|
-
i++;
|
|
7291
|
+
await client.performAction(step.action);
|
|
7292
|
+
if (options.pauseAfterActionMs) {
|
|
7293
|
+
await sleepMs(options.pauseAfterActionMs, options.signal);
|
|
7228
7294
|
}
|
|
7229
|
-
continue;
|
|
7230
|
-
}
|
|
7231
|
-
if (ch === '"') {
|
|
7232
|
-
quote = null;
|
|
7233
|
-
} else {
|
|
7234
|
-
current += ch;
|
|
7235
|
-
}
|
|
7236
|
-
continue;
|
|
7237
|
-
}
|
|
7238
|
-
if (ch === "'" || ch === '"') {
|
|
7239
|
-
quote = ch;
|
|
7240
|
-
continue;
|
|
7241
|
-
}
|
|
7242
|
-
if (/\s/.test(ch)) {
|
|
7243
|
-
if (current.length > 0) {
|
|
7244
|
-
tokens.push(current);
|
|
7245
|
-
current = "";
|
|
7246
7295
|
}
|
|
7247
|
-
|
|
7296
|
+
await options.onStep?.({ index, total, step, status: "ok" });
|
|
7297
|
+
} catch (error) {
|
|
7298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7299
|
+
await options.onStep?.({ index, total, step, status: "error", error: message });
|
|
7300
|
+
return {
|
|
7301
|
+
ok: false,
|
|
7302
|
+
completed: index,
|
|
7303
|
+
total,
|
|
7304
|
+
error: `Line ${step.lineNumber}: ${message}`
|
|
7305
|
+
};
|
|
7248
7306
|
}
|
|
7249
|
-
current += ch;
|
|
7250
7307
|
}
|
|
7251
|
-
|
|
7252
|
-
|
|
7308
|
+
return { ok: true, completed: total, total };
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7311
|
+
// ../runner-client/src/run-report.ts
|
|
7312
|
+
function actionSummary(action) {
|
|
7313
|
+
if (!action || typeof action !== "object")
|
|
7314
|
+
return "Step";
|
|
7315
|
+
const record = action;
|
|
7316
|
+
const type = typeof record.type === "string" ? record.type : "step";
|
|
7317
|
+
if (type === "tap") {
|
|
7318
|
+
if (typeof record.label === "string" && record.label.trim())
|
|
7319
|
+
return `Tap: ${record.label}`;
|
|
7320
|
+
if (typeof record.id === "string" && record.id.trim())
|
|
7321
|
+
return `Tap #${record.id}`;
|
|
7322
|
+
return "Tap";
|
|
7253
7323
|
}
|
|
7254
|
-
if (
|
|
7255
|
-
|
|
7324
|
+
if (type === "type")
|
|
7325
|
+
return `Type${typeof record.text === "string" ? `: ${record.text}` : ""}`;
|
|
7326
|
+
if (type === "wait")
|
|
7327
|
+
return "Wait";
|
|
7328
|
+
if (type === "alert") {
|
|
7329
|
+
return record.alertAction === "dismiss" ? "Dismiss alert" : "Accept alert";
|
|
7256
7330
|
}
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7331
|
+
if (type === "assert") {
|
|
7332
|
+
const assertion = typeof record.assertion === "string" ? record.assertion : "visible";
|
|
7333
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
7334
|
+
return text ? `Assert ${assertion}: ${text}` : `Assert ${assertion}`;
|
|
7335
|
+
}
|
|
7336
|
+
if (type === "verify")
|
|
7337
|
+
return "Verify";
|
|
7338
|
+
if (type === "done")
|
|
7339
|
+
return "Done";
|
|
7340
|
+
if (type === "fail")
|
|
7341
|
+
return "Failed";
|
|
7342
|
+
if (type === "swipe") {
|
|
7343
|
+
if (typeof record.direction === "string" && record.direction.trim()) {
|
|
7344
|
+
return `Swipe ${record.direction}`;
|
|
7345
|
+
}
|
|
7346
|
+
const x = typeof record.x === "number" ? record.x : null;
|
|
7347
|
+
const y = typeof record.y === "number" ? record.y : null;
|
|
7348
|
+
const x2 = typeof record.x2 === "number" ? record.x2 : null;
|
|
7349
|
+
const y2 = typeof record.y2 === "number" ? record.y2 : null;
|
|
7350
|
+
if (y != null && y2 != null && x != null && x2 != null) {
|
|
7351
|
+
const dy = y2 - y;
|
|
7352
|
+
const dx = x2 - x;
|
|
7353
|
+
if (Math.abs(dy) >= Math.abs(dx)) {
|
|
7354
|
+
if (dy < -50)
|
|
7355
|
+
return "Swipe up";
|
|
7356
|
+
if (dy > 50)
|
|
7357
|
+
return "Swipe down";
|
|
7358
|
+
} else {
|
|
7359
|
+
if (dx < -50)
|
|
7360
|
+
return "Swipe left";
|
|
7361
|
+
if (dx > 50)
|
|
7362
|
+
return "Swipe right";
|
|
7363
|
+
}
|
|
7276
7364
|
}
|
|
7365
|
+
return "Swipe";
|
|
7277
7366
|
}
|
|
7278
|
-
|
|
7279
|
-
}
|
|
7280
|
-
function flagString(flags, ...names) {
|
|
7281
|
-
for (const name of names) {
|
|
7282
|
-
const value = flags.get(name);
|
|
7283
|
-
if (typeof value === "string")
|
|
7284
|
-
return value;
|
|
7367
|
+
if (type === "input") {
|
|
7368
|
+
return `Input${typeof record.text === "string" ? `: ${record.text}` : ""}`;
|
|
7285
7369
|
}
|
|
7286
|
-
|
|
7370
|
+
if (type === "drag")
|
|
7371
|
+
return "Drag";
|
|
7372
|
+
if (type === "activate-app")
|
|
7373
|
+
return "Activate app";
|
|
7374
|
+
if (type === "terminate-app")
|
|
7375
|
+
return "Terminate app";
|
|
7376
|
+
if (type === "restart-app")
|
|
7377
|
+
return "Restart app";
|
|
7378
|
+
if (type === "background-app")
|
|
7379
|
+
return "Background app";
|
|
7380
|
+
if (type === "open-url") {
|
|
7381
|
+
return typeof record.url === "string" && record.url.trim() ? `Open URL: ${record.url}` : "Open URL";
|
|
7382
|
+
}
|
|
7383
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
7287
7384
|
}
|
|
7288
|
-
function
|
|
7289
|
-
|
|
7290
|
-
if (raw == null)
|
|
7291
|
-
return;
|
|
7292
|
-
const n = Number(raw);
|
|
7293
|
-
return Number.isFinite(n) ? n : undefined;
|
|
7385
|
+
function optionalTrimmedString(value) {
|
|
7386
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
7294
7387
|
}
|
|
7295
|
-
function
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7388
|
+
function optionalFiniteNumber(value) {
|
|
7389
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
7390
|
+
}
|
|
7391
|
+
function formatStepCommand(action) {
|
|
7392
|
+
if (!action || typeof action !== "object")
|
|
7393
|
+
return null;
|
|
7394
|
+
const record = action;
|
|
7395
|
+
const type = typeof record.type === "string" ? record.type : null;
|
|
7396
|
+
if (type == null || type === "verify" || type === "done" || type === "fail")
|
|
7397
|
+
return null;
|
|
7398
|
+
if (type === "wait") {
|
|
7399
|
+
const ms = optionalFiniteNumber(record.ms) ?? 0;
|
|
7400
|
+
return formatSleepShellLine(ms / 1000);
|
|
7306
7401
|
}
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
action.id = id;
|
|
7319
|
-
const x = flagNumber(flags, "--x");
|
|
7320
|
-
const y = flagNumber(flags, "--y");
|
|
7321
|
-
const x2 = flagNumber(flags, "--x2");
|
|
7322
|
-
const y2 = flagNumber(flags, "--y2");
|
|
7323
|
-
const durationMs = flagNumber(flags, "--duration");
|
|
7324
|
-
const text = flagString(flags, "--text");
|
|
7325
|
-
const appId = flagString(flags, "--app-id", "--bundle-id");
|
|
7326
|
-
const url = flagString(flags, "--url");
|
|
7327
|
-
const seconds = flagNumber(flags, "--seconds");
|
|
7328
|
-
if (x != null)
|
|
7329
|
-
action.x = x;
|
|
7330
|
-
if (y != null)
|
|
7331
|
-
action.y = y;
|
|
7332
|
-
if (x2 != null)
|
|
7333
|
-
action.x2 = x2;
|
|
7334
|
-
if (y2 != null)
|
|
7335
|
-
action.y2 = y2;
|
|
7336
|
-
if (durationMs != null)
|
|
7337
|
-
action.durationMs = durationMs;
|
|
7338
|
-
if (text != null)
|
|
7339
|
-
action.text = text;
|
|
7340
|
-
if (appId != null)
|
|
7341
|
-
action.appId = appId;
|
|
7342
|
-
if (url != null)
|
|
7343
|
-
action.url = url;
|
|
7344
|
-
if (seconds != null)
|
|
7345
|
-
action.seconds = seconds;
|
|
7346
|
-
if (flags.has("--double"))
|
|
7347
|
-
action.double = true;
|
|
7348
|
-
if (flags.has("--dismiss")) {
|
|
7349
|
-
action.alertAction = "dismiss";
|
|
7350
|
-
} else if (flags.has("--accept") || kind === "alert") {
|
|
7351
|
-
const alertAction = flagString(flags, "--action");
|
|
7352
|
-
if (alertAction === "dismiss")
|
|
7353
|
-
action.alertAction = "dismiss";
|
|
7354
|
-
else if (alertAction === "accept" || kind === "alert")
|
|
7355
|
-
action.alertAction = "accept";
|
|
7402
|
+
if (type === "assert") {
|
|
7403
|
+
const text = optionalTrimmedString(record.text);
|
|
7404
|
+
if (!text)
|
|
7405
|
+
return null;
|
|
7406
|
+
const assertion = record.assertion === "not-visible" ? "not-visible" : "visible";
|
|
7407
|
+
const timeoutMs = optionalFiniteNumber(record.timeoutMs);
|
|
7408
|
+
return formatAssertShellLine({
|
|
7409
|
+
assertion,
|
|
7410
|
+
text,
|
|
7411
|
+
...timeoutMs != null ? { timeoutSeconds: timeoutMs / 1000 } : {}
|
|
7412
|
+
});
|
|
7356
7413
|
}
|
|
7357
|
-
|
|
7414
|
+
let request = null;
|
|
7415
|
+
if (type === "tap") {
|
|
7416
|
+
request = { kind: "tap" };
|
|
7417
|
+
const label = optionalTrimmedString(record.label);
|
|
7418
|
+
const id = optionalTrimmedString(record.id);
|
|
7419
|
+
const x = optionalFiniteNumber(record.x);
|
|
7420
|
+
const y = optionalFiniteNumber(record.y);
|
|
7421
|
+
const durationMs = optionalFiniteNumber(record.durationMs);
|
|
7422
|
+
if (label)
|
|
7423
|
+
request.label = label;
|
|
7424
|
+
if (id)
|
|
7425
|
+
request.id = id;
|
|
7426
|
+
if (x != null)
|
|
7427
|
+
request.x = x;
|
|
7428
|
+
if (y != null)
|
|
7429
|
+
request.y = y;
|
|
7430
|
+
if (record.double === true)
|
|
7431
|
+
request.double = true;
|
|
7432
|
+
if (durationMs != null)
|
|
7433
|
+
request.durationMs = durationMs;
|
|
7434
|
+
} else if (type === "swipe" || type === "drag") {
|
|
7435
|
+
request = { kind: type };
|
|
7436
|
+
const x = optionalFiniteNumber(record.x);
|
|
7437
|
+
const y = optionalFiniteNumber(record.y);
|
|
7438
|
+
const x2 = optionalFiniteNumber(record.x2);
|
|
7439
|
+
const y2 = optionalFiniteNumber(record.y2);
|
|
7440
|
+
const durationMs = optionalFiniteNumber(record.durationMs);
|
|
7441
|
+
if (x != null)
|
|
7442
|
+
request.x = x;
|
|
7443
|
+
if (y != null)
|
|
7444
|
+
request.y = y;
|
|
7445
|
+
if (x2 != null)
|
|
7446
|
+
request.x2 = x2;
|
|
7447
|
+
if (y2 != null)
|
|
7448
|
+
request.y2 = y2;
|
|
7449
|
+
if (durationMs != null)
|
|
7450
|
+
request.durationMs = durationMs;
|
|
7451
|
+
} else if (type === "type" || type === "input") {
|
|
7452
|
+
request = { kind: "input" };
|
|
7453
|
+
const text = optionalTrimmedString(record.text);
|
|
7454
|
+
if (text)
|
|
7455
|
+
request.text = text;
|
|
7456
|
+
} else if (type === "alert") {
|
|
7457
|
+
request = {
|
|
7458
|
+
kind: "alert",
|
|
7459
|
+
alertAction: record.alertAction === "dismiss" ? "dismiss" : "accept"
|
|
7460
|
+
};
|
|
7461
|
+
} else if (type === "activate-app" || type === "terminate-app" || type === "restart-app") {
|
|
7462
|
+
request = { kind: type };
|
|
7463
|
+
const appId = optionalTrimmedString(record.appId);
|
|
7464
|
+
if (appId)
|
|
7465
|
+
request.appId = appId;
|
|
7466
|
+
} else if (type === "background-app") {
|
|
7467
|
+
request = { kind: "background-app" };
|
|
7468
|
+
const seconds = optionalFiniteNumber(record.seconds);
|
|
7469
|
+
if (seconds != null)
|
|
7470
|
+
request.seconds = seconds;
|
|
7471
|
+
} else if (type === "open-url") {
|
|
7472
|
+
request = { kind: "open-url" };
|
|
7473
|
+
const url = optionalTrimmedString(record.url);
|
|
7474
|
+
if (url)
|
|
7475
|
+
request.url = url;
|
|
7476
|
+
}
|
|
7477
|
+
return request ? formatActionShellLine(request) : null;
|
|
7478
|
+
}
|
|
7479
|
+
function stepReasoning(step) {
|
|
7480
|
+
const action = step.action && typeof step.action === "object" ? step.action : null;
|
|
7481
|
+
const reasonFromAction = typeof action?.reason === "string" && action.reason.trim() ? action.reason.trim() : null;
|
|
7482
|
+
const thoughtsFromAction = typeof action?.thoughts === "string" && action.thoughts.trim() ? action.thoughts.trim() : null;
|
|
7483
|
+
const reason = reasonFromAction ?? (step.detail?.trim() ? step.detail.trim() : null);
|
|
7484
|
+
const thoughts = thoughtsFromAction && thoughtsFromAction !== reason ? thoughtsFromAction : null;
|
|
7485
|
+
return { reason, thoughts };
|
|
7486
|
+
}
|
|
7487
|
+
function toReportStatus(status) {
|
|
7488
|
+
if (status === "passed" || status === "errored" || status === "cancelled")
|
|
7489
|
+
return status;
|
|
7490
|
+
return null;
|
|
7491
|
+
}
|
|
7492
|
+
function mapCatalogStep(step, screenshotsByStepId) {
|
|
7493
|
+
const { reason, thoughts } = stepReasoning(step);
|
|
7494
|
+
return {
|
|
7495
|
+
id: step.id,
|
|
7496
|
+
index: step.idx + 1,
|
|
7497
|
+
summary: actionSummary(step.action),
|
|
7498
|
+
ok: step.ok,
|
|
7499
|
+
latencyMs: step.latencyMs,
|
|
7500
|
+
detail: step.detail,
|
|
7501
|
+
reason,
|
|
7502
|
+
thoughts,
|
|
7503
|
+
command: step.command ?? formatStepCommand(step.action),
|
|
7504
|
+
screenshotBase64: screenshotsByStepId[step.id] ?? null
|
|
7505
|
+
};
|
|
7358
7506
|
}
|
|
7359
|
-
function
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
const pathToken = tokens[2];
|
|
7364
|
-
if (pathToken?.startsWith("-")) {
|
|
7365
|
-
throw new Error("`yoqa screenshot` accepts an optional output path only");
|
|
7366
|
-
}
|
|
7367
|
-
if (tokens.length > 3) {
|
|
7368
|
-
throw new Error("`yoqa screenshot` accepts at most one path argument");
|
|
7507
|
+
function buildRunReportFromCatalogRun(run, meta = {}, screenshotsByStepId = {}) {
|
|
7508
|
+
const status = toReportStatus(run.status);
|
|
7509
|
+
if (!status) {
|
|
7510
|
+
throw new Error(`Cannot export report for live run status: ${run.status}`);
|
|
7369
7511
|
}
|
|
7370
|
-
const
|
|
7371
|
-
|
|
7512
|
+
const caseTitles = meta.caseTitles ?? {};
|
|
7513
|
+
const tests = run.tests.map((test) => ({
|
|
7514
|
+
id: test.id,
|
|
7515
|
+
title: caseTitles[test.caseId] ?? test.caseId,
|
|
7516
|
+
status: test.status,
|
|
7517
|
+
executionMode: test.executionMode ?? null,
|
|
7518
|
+
error: test.error,
|
|
7519
|
+
startedAt: test.startedAt,
|
|
7520
|
+
finishedAt: test.finishedAt,
|
|
7521
|
+
steps: (test.steps ?? []).map((step) => mapCatalogStep(step, screenshotsByStepId))
|
|
7522
|
+
}));
|
|
7523
|
+
const primaryTitle = tests[0]?.title ?? "Run";
|
|
7524
|
+
return {
|
|
7525
|
+
id: run.id,
|
|
7526
|
+
source: "catalog",
|
|
7527
|
+
status,
|
|
7528
|
+
title: primaryTitle,
|
|
7529
|
+
appLabel: meta.appLabel ?? null,
|
|
7530
|
+
deviceLabel: meta.deviceLabel ?? null,
|
|
7531
|
+
platform: run.platform,
|
|
7532
|
+
executionMode: run.executionMode,
|
|
7533
|
+
error: run.error,
|
|
7534
|
+
createdAt: run.createdAt,
|
|
7535
|
+
startedAt: run.startedAt,
|
|
7536
|
+
finishedAt: run.finishedAt,
|
|
7537
|
+
tests
|
|
7538
|
+
};
|
|
7372
7539
|
}
|
|
7373
|
-
function
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7540
|
+
function suggestedRunReportBasename(doc) {
|
|
7541
|
+
const shortId = doc.id.replace(/[^a-zA-Z0-9]+/g, "").slice(0, 8) || "run";
|
|
7542
|
+
const raw = ["yoqa-run", shortId, doc.status].join("-").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
7543
|
+
return raw || "yoqa-run-report";
|
|
7544
|
+
}
|
|
7545
|
+
function escapeHtml(value) {
|
|
7546
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7547
|
+
}
|
|
7548
|
+
function formatWhen(ms) {
|
|
7549
|
+
if (ms == null)
|
|
7550
|
+
return "—";
|
|
7551
|
+
return new Date(ms).toLocaleString();
|
|
7552
|
+
}
|
|
7553
|
+
function formatDuration(startedAt, finishedAt) {
|
|
7554
|
+
if (startedAt == null || finishedAt == null)
|
|
7555
|
+
return "—";
|
|
7556
|
+
const ms = Math.max(0, finishedAt - startedAt);
|
|
7557
|
+
if (ms < 1000)
|
|
7558
|
+
return `${ms}ms`;
|
|
7559
|
+
const seconds = ms / 1000;
|
|
7560
|
+
if (seconds < 60)
|
|
7561
|
+
return `${seconds.toFixed(1)}s`;
|
|
7562
|
+
const minutes = Math.floor(seconds / 60);
|
|
7563
|
+
const rem = Math.round(seconds % 60);
|
|
7564
|
+
return `${minutes}m ${rem}s`;
|
|
7565
|
+
}
|
|
7566
|
+
function statusAccent(status) {
|
|
7567
|
+
if (status === "passed") {
|
|
7568
|
+
return { bg: "#dcfce7", fg: "#166534", label: "Passed" };
|
|
7385
7569
|
}
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
throw new Error("--timeout must be non-negative");
|
|
7570
|
+
if (status === "errored") {
|
|
7571
|
+
return { bg: "#fee2e2", fg: "#991b1b", label: "Failed" };
|
|
7389
7572
|
}
|
|
7390
|
-
return {
|
|
7391
|
-
kind: "assert",
|
|
7392
|
-
assertion: assertionRaw,
|
|
7393
|
-
text: text.trim(),
|
|
7394
|
-
timeoutSeconds: timeout,
|
|
7395
|
-
lineNumber,
|
|
7396
|
-
raw
|
|
7397
|
-
};
|
|
7573
|
+
return { bg: "#e5e7eb", fg: "#374151", label: "Cancelled" };
|
|
7398
7574
|
}
|
|
7399
|
-
function
|
|
7400
|
-
|
|
7401
|
-
return true;
|
|
7402
|
-
if (trimmed.startsWith("#"))
|
|
7403
|
-
return true;
|
|
7404
|
-
if (trimmed === "set -euo pipefail" || trimmed === "set -e" || trimmed === "set -eu")
|
|
7405
|
-
return true;
|
|
7406
|
-
return false;
|
|
7575
|
+
function pngDataUri(base64) {
|
|
7576
|
+
return `data:image/png;base64,${base64}`;
|
|
7407
7577
|
}
|
|
7408
|
-
function
|
|
7409
|
-
const
|
|
7410
|
-
const
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
}
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7578
|
+
function formatRunReportHtml(doc) {
|
|
7579
|
+
const accent = statusAccent(doc.status);
|
|
7580
|
+
const metaRows = [
|
|
7581
|
+
["Status", accent.label],
|
|
7582
|
+
["Source", doc.source === "catalog" ? "Catalog run" : "Manual Inspector"],
|
|
7583
|
+
["App", doc.appLabel ?? "—"],
|
|
7584
|
+
["Device", doc.deviceLabel ?? "—"],
|
|
7585
|
+
["Platform", doc.platform ?? "—"],
|
|
7586
|
+
["Mode", doc.executionMode ?? "—"],
|
|
7587
|
+
["Started", formatWhen(doc.startedAt ?? doc.createdAt)],
|
|
7588
|
+
["Finished", formatWhen(doc.finishedAt)],
|
|
7589
|
+
["Duration", formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt)],
|
|
7590
|
+
["Run ID", doc.id]
|
|
7591
|
+
];
|
|
7592
|
+
const testsHtml = doc.tests.map((test, testIndex) => {
|
|
7593
|
+
const testAccent = statusAccent(test.status === "passed" || test.status === "errored" || test.status === "cancelled" ? test.status : doc.status);
|
|
7594
|
+
const stepsHtml = test.steps.map((step) => {
|
|
7595
|
+
const stepStatus = step.ok ? "Passed" : "Failed";
|
|
7596
|
+
const stepColor = step.ok ? "#166534" : "#991b1b";
|
|
7597
|
+
const reasonBlock = step.reason ? `<p class="reason">${escapeHtml(step.reason)}</p>` : "";
|
|
7598
|
+
const thoughtsBlock = step.thoughts ? `<details class="thoughts"><summary>AI thoughts</summary><p>${escapeHtml(step.thoughts)}</p></details>` : "";
|
|
7599
|
+
const detailBlock = step.detail && step.detail !== step.reason ? `<p class="detail">${escapeHtml(step.detail)}</p>` : "";
|
|
7600
|
+
const shot = step.screenshotBase64 ? `<img class="shot" alt="Step ${step.index} screenshot" src="${pngDataUri(step.screenshotBase64)}" />` : `<p class="muted">No screenshot</p>`;
|
|
7601
|
+
const latency = step.latencyMs != null ? `<span class="muted">${step.latencyMs}ms</span>` : "";
|
|
7602
|
+
const commandBlock = step.command ? `<pre class="command"><code>${escapeHtml(step.command)}</code></pre>` : "";
|
|
7603
|
+
return `
|
|
7604
|
+
<article class="step ${step.ok ? "ok" : "fail"}">
|
|
7605
|
+
<header>
|
|
7606
|
+
<span class="idx">${step.index}</span>
|
|
7607
|
+
<div>
|
|
7608
|
+
<h3>${escapeHtml(step.summary)}</h3>
|
|
7609
|
+
<p class="step-status" style="color:${stepColor}">${stepStatus} ${latency}</p>
|
|
7610
|
+
</div>
|
|
7611
|
+
</header>
|
|
7612
|
+
${commandBlock}
|
|
7613
|
+
${reasonBlock}
|
|
7614
|
+
${detailBlock}
|
|
7615
|
+
${thoughtsBlock}
|
|
7616
|
+
${shot}
|
|
7617
|
+
</article>`;
|
|
7618
|
+
}).join(`
|
|
7619
|
+
`);
|
|
7620
|
+
return `
|
|
7621
|
+
<section class="test">
|
|
7622
|
+
<h2>
|
|
7623
|
+
<span class="test-num">${testIndex + 1}</span>
|
|
7624
|
+
${escapeHtml(test.title)}
|
|
7625
|
+
<span class="pill" style="background:${testAccent.bg};color:${testAccent.fg}">${escapeHtml(testAccent.label)}</span>
|
|
7626
|
+
</h2>
|
|
7627
|
+
${test.error ? `<p class="error">${escapeHtml(test.error)}</p>` : ""}
|
|
7628
|
+
${test.executionMode ? `<p class="muted">Mode: ${escapeHtml(test.executionMode)}</p>` : ""}
|
|
7629
|
+
<div class="steps">${stepsHtml || `<p class="muted">No steps recorded</p>`}</div>
|
|
7630
|
+
</section>`;
|
|
7631
|
+
}).join(`
|
|
7632
|
+
`);
|
|
7633
|
+
const metaHtml = metaRows.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`).join(`
|
|
7634
|
+
`);
|
|
7635
|
+
return `<!DOCTYPE html>
|
|
7636
|
+
<html lang="en">
|
|
7637
|
+
<head>
|
|
7638
|
+
<meta charset="utf-8" />
|
|
7639
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7640
|
+
<title>Yoqa Report — ${escapeHtml(doc.title)} (${escapeHtml(accent.label)})</title>
|
|
7641
|
+
<style>
|
|
7642
|
+
:root { color-scheme: light; }
|
|
7643
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #f8fafc; color: #0f172a; line-height: 1.5; }
|
|
7644
|
+
.wrap { max-width: 52rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
7645
|
+
.banner { border-radius: 1rem; padding: 1.25rem 1.5rem; background: ${accent.bg}; color: ${accent.fg}; margin-bottom: 1.5rem; }
|
|
7646
|
+
.banner h1 { margin: 0 0 0.35rem; font-size: 1.5rem; }
|
|
7647
|
+
.banner p { margin: 0; opacity: 0.9; }
|
|
7648
|
+
table.meta { width: 100%; border-collapse: collapse; background: #fff; border-radius: 0.75rem; overflow: hidden; box-shadow: 0 1px 2px rgb(0 0 0 / 0.06); margin-bottom: 2rem; }
|
|
7649
|
+
table.meta th, table.meta td { text-align: left; padding: 0.65rem 1rem; border-bottom: 1px solid #e2e8f0; font-size: 0.9rem; }
|
|
7650
|
+
table.meta th { width: 8rem; color: #64748b; font-weight: 600; }
|
|
7651
|
+
.error { background: #fee2e2; color: #991b1b; padding: 0.75rem 1rem; border-radius: 0.5rem; }
|
|
7652
|
+
.test { margin-bottom: 2.5rem; }
|
|
7653
|
+
.test h2 { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 1.15rem; }
|
|
7654
|
+
.test-num { display: inline-flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; border-radius: 999px; background: #0f172a; color: #fff; font-size: 0.8rem; }
|
|
7655
|
+
.pill { display: inline-flex; align-items: center; border-radius: 999px; padding: 0.15rem 0.65rem; font-size: 0.75rem; font-weight: 600; }
|
|
7656
|
+
.step { background: #fff; border: 1px solid #e2e8f0; border-radius: 0.75rem; padding: 1rem; margin: 0.75rem 0; }
|
|
7657
|
+
.step.fail { border-color: #fecaca; }
|
|
7658
|
+
.step header { display: flex; gap: 0.75rem; align-items: flex-start; }
|
|
7659
|
+
.step .idx { flex-shrink: 0; width: 1.75rem; height: 1.75rem; border-radius: 999px; background: #e2e8f0; display: inline-flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; }
|
|
7660
|
+
.step.ok .idx { background: #bbf7d0; color: #166534; }
|
|
7661
|
+
.step.fail .idx { background: #fecaca; color: #991b1b; }
|
|
7662
|
+
.step h3 { margin: 0; font-size: 1rem; }
|
|
7663
|
+
.step-status { margin: 0.15rem 0 0; font-size: 0.8rem; font-weight: 600; }
|
|
7664
|
+
.command { margin: 0.5rem 0 0; padding: 0.4rem 0.65rem; background: #f1f5f9; border-radius: 0.4rem; font-size: 0.8rem; overflow-x: auto; }
|
|
7665
|
+
.command code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
7666
|
+
.reason, .detail { margin: 0.5rem 0 0; color: #475569; font-size: 0.9rem; }
|
|
7667
|
+
.thoughts { margin-top: 0.5rem; font-size: 0.85rem; color: #64748b; }
|
|
7668
|
+
.thoughts p { margin: 0.35rem 0 0; white-space: pre-wrap; }
|
|
7669
|
+
.shot { display: block; max-width: 100%; width: min(22rem, 100%); margin-top: 0.75rem; border-radius: 0.75rem; border: 1px solid #e2e8f0; }
|
|
7670
|
+
.muted { color: #94a3b8; font-size: 0.85rem; }
|
|
7671
|
+
footer { margin-top: 2rem; color: #94a3b8; font-size: 0.8rem; }
|
|
7672
|
+
@media print {
|
|
7673
|
+
body { background: #fff; }
|
|
7674
|
+
.shot { max-height: 60vh; object-fit: contain; }
|
|
7450
7675
|
}
|
|
7451
|
-
|
|
7676
|
+
</style>
|
|
7677
|
+
</head>
|
|
7678
|
+
<body>
|
|
7679
|
+
<div class="wrap">
|
|
7680
|
+
<header class="banner">
|
|
7681
|
+
<h1>${escapeHtml(doc.title)}</h1>
|
|
7682
|
+
<p>${escapeHtml(accent.label)} · Yoqa end-to-end report</p>
|
|
7683
|
+
</header>
|
|
7684
|
+
${doc.error ? `<p class="error">${escapeHtml(doc.error)}</p>` : ""}
|
|
7685
|
+
<table class="meta">${metaHtml}</table>
|
|
7686
|
+
${testsHtml}
|
|
7687
|
+
<footer>Generated by Yoqa</footer>
|
|
7688
|
+
</div>
|
|
7689
|
+
</body>
|
|
7690
|
+
</html>
|
|
7691
|
+
`;
|
|
7452
7692
|
}
|
|
7453
|
-
function
|
|
7454
|
-
|
|
7455
|
-
const timeout = input.timeoutSeconds ?? 5;
|
|
7456
|
-
if (timeout !== 5) {
|
|
7457
|
-
parts.push("--timeout", String(timeout));
|
|
7458
|
-
}
|
|
7459
|
-
return parts.join(" ");
|
|
7693
|
+
function escapeMd(value) {
|
|
7694
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
7460
7695
|
}
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7696
|
+
function formatRunReportMarkdown(doc) {
|
|
7697
|
+
const accent = statusAccent(doc.status);
|
|
7698
|
+
const lines = [
|
|
7699
|
+
`# Yoqa Report — ${doc.title}`,
|
|
7700
|
+
"",
|
|
7701
|
+
`**Status:** ${accent.label}`,
|
|
7702
|
+
"",
|
|
7703
|
+
"| Field | Value |",
|
|
7704
|
+
"| --- | --- |",
|
|
7705
|
+
`| Source | ${doc.source === "catalog" ? "Catalog run" : "Manual Inspector"} |`,
|
|
7706
|
+
`| App | ${escapeMd(doc.appLabel ?? "—")} |`,
|
|
7707
|
+
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
7708
|
+
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
7709
|
+
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
7710
|
+
`| Started | ${escapeMd(formatWhen(doc.startedAt ?? doc.createdAt))} |`,
|
|
7711
|
+
`| Finished | ${escapeMd(formatWhen(doc.finishedAt))} |`,
|
|
7712
|
+
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
7713
|
+
`| Run ID | \`${doc.id}\` |`,
|
|
7714
|
+
""
|
|
7715
|
+
];
|
|
7716
|
+
if (doc.error) {
|
|
7717
|
+
lines.push(`> **Error:** ${doc.error}`, "");
|
|
7718
|
+
}
|
|
7719
|
+
doc.tests.forEach((test, testIndex) => {
|
|
7720
|
+
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
7721
|
+
lines.push(`## ${testIndex + 1}. ${test.title} (${testLabel})`, "");
|
|
7722
|
+
if (test.executionMode)
|
|
7723
|
+
lines.push(`Mode: \`${test.executionMode}\``, "");
|
|
7724
|
+
if (test.error)
|
|
7725
|
+
lines.push(`**Error:** ${test.error}`, "");
|
|
7726
|
+
if (test.steps.length === 0) {
|
|
7727
|
+
lines.push("_No steps recorded_", "");
|
|
7475
7728
|
return;
|
|
7476
7729
|
}
|
|
7477
|
-
const
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
return label.includes(needle) || type.includes(needle);
|
|
7499
|
-
});
|
|
7500
|
-
}
|
|
7501
|
-
async function runAssertStep(client, step, signal) {
|
|
7502
|
-
const deadline = Date.now() + step.timeoutSeconds * 1000;
|
|
7503
|
-
for (;; ) {
|
|
7504
|
-
if (signal?.aborted) {
|
|
7505
|
-
throw Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
7506
|
-
}
|
|
7507
|
-
const screen = await client.getScreen();
|
|
7508
|
-
const found = screenHasText(screen.elements, step.text);
|
|
7509
|
-
if (step.assertion === "visible" && found)
|
|
7510
|
-
return;
|
|
7511
|
-
if (step.assertion === "not-visible" && !found)
|
|
7512
|
-
return;
|
|
7513
|
-
if (Date.now() >= deadline) {
|
|
7514
|
-
throw new Error(step.assertion === "visible" ? `Expected visible text not found within ${step.timeoutSeconds}s: ${step.text}` : `Unexpected text still visible after ${step.timeoutSeconds}s: ${step.text}`);
|
|
7730
|
+
for (const step of test.steps) {
|
|
7731
|
+
lines.push(`### Step ${step.index}: ${step.summary}`);
|
|
7732
|
+
lines.push("");
|
|
7733
|
+
lines.push(`- Result: **${step.ok ? "Passed" : "Failed"}**`);
|
|
7734
|
+
if (step.command)
|
|
7735
|
+
lines.push(`- Command: \`${step.command.replace(/`/g, "'")}\``);
|
|
7736
|
+
if (step.latencyMs != null)
|
|
7737
|
+
lines.push(`- Latency: ${step.latencyMs}ms`);
|
|
7738
|
+
if (step.reason)
|
|
7739
|
+
lines.push(`- Reason: ${step.reason}`);
|
|
7740
|
+
if (step.detail && step.detail !== step.reason)
|
|
7741
|
+
lines.push(`- Detail: ${step.detail}`);
|
|
7742
|
+
if (step.thoughts) {
|
|
7743
|
+
lines.push("", "<details><summary>AI thoughts</summary>", "", step.thoughts, "", "</details>");
|
|
7744
|
+
}
|
|
7745
|
+
if (step.screenshotBase64) {
|
|
7746
|
+
lines.push("", `})`);
|
|
7747
|
+
} else {
|
|
7748
|
+
lines.push("", "_No screenshot_");
|
|
7749
|
+
}
|
|
7750
|
+
lines.push("");
|
|
7515
7751
|
}
|
|
7516
|
-
|
|
7517
|
-
|
|
7752
|
+
});
|
|
7753
|
+
lines.push("---", "", "_Generated by Yoqa_", "");
|
|
7754
|
+
return lines.join(`
|
|
7755
|
+
`);
|
|
7518
7756
|
}
|
|
7519
|
-
|
|
7520
|
-
const
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7757
|
+
function formatRunReportGithubSummary(doc) {
|
|
7758
|
+
const accent = statusAccent(doc.status);
|
|
7759
|
+
const passed = doc.tests.filter((test) => test.status === "passed").length;
|
|
7760
|
+
const failed = doc.tests.filter((test) => test.status === "errored").length;
|
|
7761
|
+
const cancelled = doc.tests.filter((test) => test.status === "cancelled").length;
|
|
7762
|
+
const failedSteps = doc.tests.flatMap((test) => test.steps.filter((step) => !step.ok).map((step) => ({ test, step })));
|
|
7763
|
+
const lines = [
|
|
7764
|
+
`## Yoqa Report — ${doc.title}`,
|
|
7765
|
+
"",
|
|
7766
|
+
`**${accent.label}** · ${doc.tests.length} test${doc.tests.length === 1 ? "" : "s"} · ${passed} passed · ${failed} failed · ${cancelled} cancelled`,
|
|
7767
|
+
"",
|
|
7768
|
+
"| Field | Value |",
|
|
7769
|
+
"| --- | --- |",
|
|
7770
|
+
`| App | ${escapeMd(doc.appLabel ?? "—")} |`,
|
|
7771
|
+
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
7772
|
+
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
7773
|
+
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
7774
|
+
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
7775
|
+
`| Run ID | \`${doc.id}\` |`,
|
|
7776
|
+
""
|
|
7777
|
+
];
|
|
7778
|
+
if (doc.error) {
|
|
7779
|
+
lines.push(`> **Error:** ${doc.error}`, "");
|
|
7532
7780
|
}
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7781
|
+
if (doc.tests.length > 0) {
|
|
7782
|
+
lines.push("### Tests", "", "| # | Test | Status | Steps |", "| --- | --- | --- | --- |");
|
|
7783
|
+
doc.tests.forEach((test, testIndex) => {
|
|
7784
|
+
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
7785
|
+
lines.push(`| ${testIndex + 1} | ${escapeMd(test.title)} | ${testLabel} | ${test.steps.length} |`);
|
|
7786
|
+
});
|
|
7787
|
+
lines.push("");
|
|
7537
7788
|
}
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
await sleepMs(step.seconds * 1000, options.signal);
|
|
7549
|
-
} else if (step.kind === "assert") {
|
|
7550
|
-
await runAssertStep(client, step, options.signal);
|
|
7551
|
-
} else if (step.kind === "screenshot") {
|
|
7552
|
-
await client.takeScreenshot(step.path ? { path: step.path } : {});
|
|
7553
|
-
} else {
|
|
7554
|
-
await client.performAction(step.action);
|
|
7555
|
-
if (options.pauseAfterActionMs) {
|
|
7556
|
-
await sleepMs(options.pauseAfterActionMs, options.signal);
|
|
7557
|
-
}
|
|
7789
|
+
if (failedSteps.length > 0) {
|
|
7790
|
+
lines.push("### Failed steps", "");
|
|
7791
|
+
for (const { test, step } of failedSteps) {
|
|
7792
|
+
lines.push(`- **${escapeMd(test.title)}** · step ${step.index}: ${escapeMd(step.summary)}`);
|
|
7793
|
+
if (step.command)
|
|
7794
|
+
lines.push(` - Command: \`${escapeMd(step.command)}\``);
|
|
7795
|
+
if (step.reason)
|
|
7796
|
+
lines.push(` - Reason: ${escapeMd(step.reason)}`);
|
|
7797
|
+
if (step.detail && step.detail !== step.reason) {
|
|
7798
|
+
lines.push(` - Detail: ${escapeMd(step.detail)}`);
|
|
7558
7799
|
}
|
|
7559
|
-
await options.onStep?.({ index, total, step, status: "ok" });
|
|
7560
|
-
} catch (error) {
|
|
7561
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7562
|
-
await options.onStep?.({ index, total, step, status: "error", error: message });
|
|
7563
|
-
return {
|
|
7564
|
-
ok: false,
|
|
7565
|
-
completed: index,
|
|
7566
|
-
total,
|
|
7567
|
-
error: `Line ${step.lineNumber}: ${message}`
|
|
7568
|
-
};
|
|
7569
7800
|
}
|
|
7801
|
+
lines.push("");
|
|
7570
7802
|
}
|
|
7571
|
-
|
|
7803
|
+
lines.push("Download the `yoqa-report` artifact for the full HTML report with screenshots.", "");
|
|
7804
|
+
return lines.join(`
|
|
7805
|
+
`);
|
|
7572
7806
|
}
|
|
7573
7807
|
// ../runner-client/src/index.ts
|
|
7574
7808
|
var DEFAULT_BASE_URL = "http://127.0.0.1:7420";
|
|
@@ -8043,7 +8277,7 @@ var {
|
|
|
8043
8277
|
// package.json
|
|
8044
8278
|
var package_default = {
|
|
8045
8279
|
name: "@yoqa/cli",
|
|
8046
|
-
version: "0.3.
|
|
8280
|
+
version: "0.3.26",
|
|
8047
8281
|
description: "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
|
|
8048
8282
|
license: "UNLICENSED",
|
|
8049
8283
|
type: "module",
|
|
@@ -8081,7 +8315,7 @@ var package_default = {
|
|
|
8081
8315
|
health: "bun run src/main.ts health"
|
|
8082
8316
|
},
|
|
8083
8317
|
dependencies: {
|
|
8084
|
-
"@yoqa/runner": "0.3.
|
|
8318
|
+
"@yoqa/runner": "0.3.26"
|
|
8085
8319
|
},
|
|
8086
8320
|
devDependencies: {
|
|
8087
8321
|
"@types/node": "^22.15.30",
|
|
@@ -9823,18 +10057,80 @@ scriptCmd.command("run").argument("<file>", "Path to a .yoqa.json / CaseScript J
|
|
|
9823
10057
|
await sleep(800);
|
|
9824
10058
|
continue;
|
|
9825
10059
|
}
|
|
10060
|
+
if (action2.type === "swipe" || action2.type === "drag") {
|
|
10061
|
+
const body2 = await c.performAction({
|
|
10062
|
+
kind: action2.type,
|
|
10063
|
+
x: action2.x,
|
|
10064
|
+
y: action2.y,
|
|
10065
|
+
x2: action2.x2,
|
|
10066
|
+
y2: action2.y2,
|
|
10067
|
+
durationMs: action2.durationMs
|
|
10068
|
+
});
|
|
10069
|
+
if (options.json) {
|
|
10070
|
+
console.log(JSON.stringify({ ...body2, step: idx }, null, 2));
|
|
10071
|
+
} else {
|
|
10072
|
+
console.log(`ok ${body2.kind} (${idx}/${script.actions.length})`);
|
|
10073
|
+
}
|
|
10074
|
+
await sleep(800);
|
|
10075
|
+
continue;
|
|
10076
|
+
}
|
|
10077
|
+
if (action2.type === "activate-app" || action2.type === "terminate-app" || action2.type === "restart-app") {
|
|
10078
|
+
const body2 = await c.performAction({ kind: action2.type, appId: action2.appId });
|
|
10079
|
+
if (options.json) {
|
|
10080
|
+
console.log(JSON.stringify({ ...body2, step: idx }, null, 2));
|
|
10081
|
+
} else {
|
|
10082
|
+
console.log(`ok ${body2.kind} (${idx}/${script.actions.length})`);
|
|
10083
|
+
}
|
|
10084
|
+
await sleep(800);
|
|
10085
|
+
continue;
|
|
10086
|
+
}
|
|
10087
|
+
if (action2.type === "background-app") {
|
|
10088
|
+
const body2 = await c.performAction({
|
|
10089
|
+
kind: "background-app",
|
|
10090
|
+
seconds: action2.seconds
|
|
10091
|
+
});
|
|
10092
|
+
if (options.json) {
|
|
10093
|
+
console.log(JSON.stringify({ ...body2, step: idx }, null, 2));
|
|
10094
|
+
} else {
|
|
10095
|
+
console.log(`ok ${body2.kind} (${idx}/${script.actions.length})`);
|
|
10096
|
+
}
|
|
10097
|
+
await sleep(800);
|
|
10098
|
+
continue;
|
|
10099
|
+
}
|
|
10100
|
+
if (action2.type === "open-url") {
|
|
10101
|
+
const body2 = await c.performAction({ kind: "open-url", url: action2.url });
|
|
10102
|
+
if (options.json) {
|
|
10103
|
+
console.log(JSON.stringify({ ...body2, step: idx }, null, 2));
|
|
10104
|
+
} else {
|
|
10105
|
+
console.log(`ok ${body2.kind} (${idx}/${script.actions.length})`);
|
|
10106
|
+
}
|
|
10107
|
+
await sleep(800);
|
|
10108
|
+
continue;
|
|
10109
|
+
}
|
|
10110
|
+
if (action2.type === "type") {
|
|
10111
|
+
const body2 = await c.performAction({ kind: "input", text: action2.text });
|
|
10112
|
+
if (options.json) {
|
|
10113
|
+
console.log(JSON.stringify({ ...body2, step: idx }, null, 2));
|
|
10114
|
+
} else {
|
|
10115
|
+
console.log(`ok ${body2.kind} (${idx}/${script.actions.length})`);
|
|
10116
|
+
}
|
|
10117
|
+
await sleep(800);
|
|
10118
|
+
continue;
|
|
10119
|
+
}
|
|
9826
10120
|
const tapBody = { kind: "tap" };
|
|
9827
|
-
if (action2.
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
10121
|
+
if (action2.label)
|
|
10122
|
+
tapBody.label = action2.label;
|
|
10123
|
+
if (action2.id)
|
|
10124
|
+
tapBody.id = action2.id;
|
|
10125
|
+
if (action2.x != null)
|
|
10126
|
+
tapBody.x = action2.x;
|
|
10127
|
+
if (action2.y != null)
|
|
10128
|
+
tapBody.y = action2.y;
|
|
10129
|
+
if (action2.double)
|
|
10130
|
+
tapBody.double = true;
|
|
10131
|
+
if (action2.durationMs != null)
|
|
10132
|
+
tapBody.durationMs = action2.durationMs;
|
|
10133
|
+
const body = await c.performAction(tapBody);
|
|
9838
10134
|
if (options.json) {
|
|
9839
10135
|
console.log(JSON.stringify({ ...body, step: idx }, null, 2));
|
|
9840
10136
|
} else {
|