@yoqa/cli 1.0.2 → 1.0.3
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 +1770 -1565
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -13045,13 +13045,20 @@ var healthResponseSchema = exports_external.object({
|
|
|
13045
13045
|
version: exports_external.string(),
|
|
13046
13046
|
uptimeMs: exports_external.number().nonnegative()
|
|
13047
13047
|
});
|
|
13048
|
-
var devicePlatformSchema = exports_external.union([
|
|
13048
|
+
var devicePlatformSchema = exports_external.union([
|
|
13049
|
+
exports_external.literal("ios"),
|
|
13050
|
+
exports_external.literal("android"),
|
|
13051
|
+
exports_external.literal("web")
|
|
13052
|
+
]);
|
|
13049
13053
|
var laneNameSchema = exports_external.literal("direct");
|
|
13050
13054
|
var deviceKindSchema = exports_external.union([
|
|
13051
13055
|
exports_external.literal("physical"),
|
|
13052
13056
|
exports_external.literal("simulator"),
|
|
13053
|
-
exports_external.literal("emulator")
|
|
13057
|
+
exports_external.literal("emulator"),
|
|
13058
|
+
exports_external.literal("browser")
|
|
13054
13059
|
]);
|
|
13060
|
+
var VIEWPORT_PRESET_NAMES = ["desktop", "mobile"];
|
|
13061
|
+
var viewportPresetSchema = exports_external.enum(VIEWPORT_PRESET_NAMES);
|
|
13055
13062
|
var deviceSchema = exports_external.object({
|
|
13056
13063
|
id: exports_external.string().min(1),
|
|
13057
13064
|
name: exports_external.string().min(1),
|
|
@@ -13191,13 +13198,15 @@ var catalogAppSchema = exports_external.object({
|
|
|
13191
13198
|
iosBundleId: exports_external.string(),
|
|
13192
13199
|
iosAppStoreId: exports_external.string(),
|
|
13193
13200
|
androidApplicationId: exports_external.string(),
|
|
13201
|
+
webUrl: exports_external.string().default(""),
|
|
13194
13202
|
capabilities: exports_external.array(capabilitySchema),
|
|
13195
13203
|
createdAt: exports_external.number().int().nonnegative(),
|
|
13196
13204
|
updatedAt: exports_external.number().int().nonnegative()
|
|
13197
13205
|
});
|
|
13198
13206
|
var createAppRequestSchema = exports_external.object({
|
|
13199
13207
|
name: exports_external.string().min(1),
|
|
13200
|
-
prefix: exports_external.string().min(1).optional()
|
|
13208
|
+
prefix: exports_external.string().min(1).optional(),
|
|
13209
|
+
webUrl: exports_external.string().optional()
|
|
13201
13210
|
});
|
|
13202
13211
|
var updateAppRequestSchema = exports_external.object({
|
|
13203
13212
|
name: exports_external.string().min(1).optional(),
|
|
@@ -13207,6 +13216,7 @@ var updateAppRequestSchema = exports_external.object({
|
|
|
13207
13216
|
iosBundleId: exports_external.string().optional(),
|
|
13208
13217
|
iosAppStoreId: exports_external.string().optional(),
|
|
13209
13218
|
androidApplicationId: exports_external.string().optional(),
|
|
13219
|
+
webUrl: exports_external.string().optional(),
|
|
13210
13220
|
capabilities: exports_external.array(capabilitySchema).optional()
|
|
13211
13221
|
});
|
|
13212
13222
|
var listAppsResponseSchema = exports_external.object({
|
|
@@ -13255,6 +13265,25 @@ var caseScriptDragSchema = exports_external.object({
|
|
|
13255
13265
|
durationMs: exports_external.number().min(50).max(5000).optional(),
|
|
13256
13266
|
reason: exports_external.string().optional()
|
|
13257
13267
|
});
|
|
13268
|
+
var KEY_NAMES = [
|
|
13269
|
+
"Enter",
|
|
13270
|
+
"Tab",
|
|
13271
|
+
"Escape",
|
|
13272
|
+
"Backspace",
|
|
13273
|
+
"Delete",
|
|
13274
|
+
"ArrowUp",
|
|
13275
|
+
"ArrowDown",
|
|
13276
|
+
"ArrowLeft",
|
|
13277
|
+
"ArrowRight",
|
|
13278
|
+
"Home",
|
|
13279
|
+
"End",
|
|
13280
|
+
"PageUp",
|
|
13281
|
+
"PageDown",
|
|
13282
|
+
"Space"
|
|
13283
|
+
];
|
|
13284
|
+
var keyNameSchema = exports_external.enum(KEY_NAMES);
|
|
13285
|
+
var KEY_MODIFIERS = ["Shift", "Control", "Alt", "Meta"];
|
|
13286
|
+
var keyModifierSchema = exports_external.enum(KEY_MODIFIERS);
|
|
13258
13287
|
function caseScriptAppIdSchema(type) {
|
|
13259
13288
|
return exports_external.object({
|
|
13260
13289
|
type: exports_external.literal(type),
|
|
@@ -13262,60 +13291,99 @@ function caseScriptAppIdSchema(type) {
|
|
|
13262
13291
|
reason: exports_external.string().optional()
|
|
13263
13292
|
});
|
|
13264
13293
|
}
|
|
13294
|
+
var caseScriptTypeSchema = exports_external.object({
|
|
13295
|
+
type: exports_external.literal("type"),
|
|
13296
|
+
text: exports_external.string(),
|
|
13297
|
+
reason: exports_external.string().optional()
|
|
13298
|
+
});
|
|
13299
|
+
var caseScriptWaitSchema = exports_external.object({
|
|
13300
|
+
type: exports_external.literal("wait"),
|
|
13301
|
+
ms: exports_external.number().min(0).max(1e4),
|
|
13302
|
+
reason: exports_external.string().optional()
|
|
13303
|
+
});
|
|
13304
|
+
var caseScriptAssertionSchema = exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).default("visible");
|
|
13305
|
+
var caseScriptAssertSchema = exports_external.object({
|
|
13306
|
+
type: exports_external.literal("assert"),
|
|
13307
|
+
assertion: caseScriptAssertionSchema,
|
|
13308
|
+
text: exports_external.string().min(1),
|
|
13309
|
+
timeoutMs: exports_external.number().min(0).max(120000).optional(),
|
|
13310
|
+
reason: exports_external.string().optional()
|
|
13311
|
+
});
|
|
13312
|
+
var caseScriptAlertSchema = exports_external.object({
|
|
13313
|
+
type: exports_external.literal("alert"),
|
|
13314
|
+
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).default("accept"),
|
|
13315
|
+
reason: exports_external.string().optional()
|
|
13316
|
+
});
|
|
13317
|
+
var caseScriptBackgroundAppSchema = exports_external.object({
|
|
13318
|
+
type: exports_external.literal("background-app"),
|
|
13319
|
+
seconds: exports_external.number().min(0).max(120).optional(),
|
|
13320
|
+
reason: exports_external.string().optional()
|
|
13321
|
+
});
|
|
13322
|
+
var caseScriptOpenUrlSchema = exports_external.object({
|
|
13323
|
+
type: exports_external.literal("open-url"),
|
|
13324
|
+
url: exports_external.string().min(1),
|
|
13325
|
+
reason: exports_external.string().optional()
|
|
13326
|
+
});
|
|
13327
|
+
var caseScriptKeySchema = exports_external.object({
|
|
13328
|
+
type: exports_external.literal("key"),
|
|
13329
|
+
key: keyNameSchema,
|
|
13330
|
+
modifiers: exports_external.array(keyModifierSchema).min(1).optional(),
|
|
13331
|
+
reason: exports_external.string().optional()
|
|
13332
|
+
});
|
|
13333
|
+
var caseScriptGestureSchema = exports_external.union([
|
|
13334
|
+
caseScriptTapSchema,
|
|
13335
|
+
caseScriptSwipeSchema,
|
|
13336
|
+
caseScriptDragSchema,
|
|
13337
|
+
caseScriptTypeSchema,
|
|
13338
|
+
caseScriptWaitSchema,
|
|
13339
|
+
caseScriptAssertSchema,
|
|
13340
|
+
caseScriptAlertSchema,
|
|
13341
|
+
caseScriptAppIdSchema("activate-app"),
|
|
13342
|
+
caseScriptAppIdSchema("terminate-app"),
|
|
13343
|
+
caseScriptAppIdSchema("restart-app"),
|
|
13344
|
+
caseScriptBackgroundAppSchema,
|
|
13345
|
+
caseScriptOpenUrlSchema,
|
|
13346
|
+
caseScriptKeySchema
|
|
13347
|
+
]);
|
|
13348
|
+
var BRANCH_DEFAULT_TIMEOUT_MS = 2000;
|
|
13349
|
+
var caseScriptStepsSchema = exports_external.array(exports_external.lazy(() => caseScriptActionSchema));
|
|
13350
|
+
var caseScriptBranchSchema = exports_external.object({
|
|
13351
|
+
type: exports_external.literal("branch"),
|
|
13352
|
+
text: exports_external.string().min(1).optional(),
|
|
13353
|
+
id: exports_external.string().min(1).optional(),
|
|
13354
|
+
assertion: caseScriptAssertionSchema,
|
|
13355
|
+
timeoutMs: exports_external.number().min(0).max(120000).optional(),
|
|
13356
|
+
then: caseScriptStepsSchema.min(1),
|
|
13357
|
+
else: caseScriptStepsSchema.optional().transform((actions) => actions?.length ? actions : undefined),
|
|
13358
|
+
reason: exports_external.string().optional()
|
|
13359
|
+
}).superRefine((value, ctx) => {
|
|
13360
|
+
if (value.text == null === (value.id == null)) {
|
|
13361
|
+
ctx.addIssue({
|
|
13362
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13363
|
+
message: "branch requires exactly one of text or id"
|
|
13364
|
+
});
|
|
13365
|
+
}
|
|
13366
|
+
});
|
|
13367
|
+
var caseScriptActionSchema = exports_external.union([
|
|
13368
|
+
caseScriptGestureSchema,
|
|
13369
|
+
caseScriptBranchSchema
|
|
13370
|
+
]);
|
|
13265
13371
|
var caseScriptActionSchemaByType = {
|
|
13266
13372
|
tap: caseScriptTapSchema,
|
|
13267
13373
|
swipe: caseScriptSwipeSchema,
|
|
13268
13374
|
drag: caseScriptDragSchema,
|
|
13269
|
-
type:
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
}),
|
|
13274
|
-
wait: exports_external.object({
|
|
13275
|
-
type: exports_external.literal("wait"),
|
|
13276
|
-
ms: exports_external.number().min(0).max(1e4),
|
|
13277
|
-
reason: exports_external.string().optional()
|
|
13278
|
-
}),
|
|
13279
|
-
assert: exports_external.object({
|
|
13280
|
-
type: exports_external.literal("assert"),
|
|
13281
|
-
assertion: exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).default("visible"),
|
|
13282
|
-
text: exports_external.string().min(1),
|
|
13283
|
-
timeoutMs: exports_external.number().min(0).max(120000).optional(),
|
|
13284
|
-
reason: exports_external.string().optional()
|
|
13285
|
-
}),
|
|
13286
|
-
alert: exports_external.object({
|
|
13287
|
-
type: exports_external.literal("alert"),
|
|
13288
|
-
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).default("accept"),
|
|
13289
|
-
reason: exports_external.string().optional()
|
|
13290
|
-
}),
|
|
13375
|
+
type: caseScriptTypeSchema,
|
|
13376
|
+
wait: caseScriptWaitSchema,
|
|
13377
|
+
assert: caseScriptAssertSchema,
|
|
13378
|
+
alert: caseScriptAlertSchema,
|
|
13291
13379
|
"activate-app": caseScriptAppIdSchema("activate-app"),
|
|
13292
13380
|
"terminate-app": caseScriptAppIdSchema("terminate-app"),
|
|
13293
13381
|
"restart-app": caseScriptAppIdSchema("restart-app"),
|
|
13294
|
-
"background-app":
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
}),
|
|
13299
|
-
"open-url": exports_external.object({
|
|
13300
|
-
type: exports_external.literal("open-url"),
|
|
13301
|
-
url: exports_external.string().min(1),
|
|
13302
|
-
reason: exports_external.string().optional()
|
|
13303
|
-
})
|
|
13382
|
+
"background-app": caseScriptBackgroundAppSchema,
|
|
13383
|
+
"open-url": caseScriptOpenUrlSchema,
|
|
13384
|
+
key: caseScriptKeySchema,
|
|
13385
|
+
branch: caseScriptBranchSchema
|
|
13304
13386
|
};
|
|
13305
|
-
var caseScriptActionSchema = exports_external.union([
|
|
13306
|
-
caseScriptActionSchemaByType.tap,
|
|
13307
|
-
caseScriptActionSchemaByType.swipe,
|
|
13308
|
-
caseScriptActionSchemaByType.drag,
|
|
13309
|
-
caseScriptActionSchemaByType.type,
|
|
13310
|
-
caseScriptActionSchemaByType.wait,
|
|
13311
|
-
caseScriptActionSchemaByType.assert,
|
|
13312
|
-
caseScriptActionSchemaByType.alert,
|
|
13313
|
-
caseScriptActionSchemaByType["activate-app"],
|
|
13314
|
-
caseScriptActionSchemaByType["terminate-app"],
|
|
13315
|
-
caseScriptActionSchemaByType["restart-app"],
|
|
13316
|
-
caseScriptActionSchemaByType["background-app"],
|
|
13317
|
-
caseScriptActionSchemaByType["open-url"]
|
|
13318
|
-
]);
|
|
13319
13387
|
var caseScriptSchema = exports_external.object({
|
|
13320
13388
|
version: exports_external.literal(1),
|
|
13321
13389
|
sourceRunId: exports_external.string().min(1).optional(),
|
|
@@ -13626,6 +13694,8 @@ var runSchema = exports_external.object({
|
|
|
13626
13694
|
screenMode: runScreenModeSchema.optional(),
|
|
13627
13695
|
lane: laneNameSchema.optional(),
|
|
13628
13696
|
laneWarning: exports_external.string().min(1).optional(),
|
|
13697
|
+
browser: exports_external.string().min(1).optional(),
|
|
13698
|
+
viewport: viewportPresetSchema.optional(),
|
|
13629
13699
|
error: exports_external.string().nullable(),
|
|
13630
13700
|
createdAt: exports_external.number().int().nonnegative(),
|
|
13631
13701
|
startedAt: exports_external.number().int().nonnegative().nullable(),
|
|
@@ -13644,7 +13714,8 @@ var connectDeviceRequestSchema = exports_external.object({
|
|
|
13644
13714
|
platform: devicePlatformSchema,
|
|
13645
13715
|
bundleId: exports_external.string().min(1).optional(),
|
|
13646
13716
|
appPackage: exports_external.string().min(1).optional(),
|
|
13647
|
-
lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
|
|
13717
|
+
lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional(),
|
|
13718
|
+
viewport: viewportPresetSchema.optional()
|
|
13648
13719
|
});
|
|
13649
13720
|
var streamStateSchema = exports_external.union([
|
|
13650
13721
|
exports_external.literal("ready"),
|
|
@@ -13662,7 +13733,8 @@ var activeDeviceResponseSchema = exports_external.object({
|
|
|
13662
13733
|
heldByRun: exports_external.boolean().optional(),
|
|
13663
13734
|
heldByRunId: exports_external.string().min(1).optional(),
|
|
13664
13735
|
lane: laneNameSchema.optional(),
|
|
13665
|
-
laneWarning: exports_external.string().min(1).optional()
|
|
13736
|
+
laneWarning: exports_external.string().min(1).optional(),
|
|
13737
|
+
viewport: viewportPresetSchema.optional()
|
|
13666
13738
|
});
|
|
13667
13739
|
var retargetDeviceRequestSchema = exports_external.object({
|
|
13668
13740
|
bundleId: exports_external.string().min(1).optional(),
|
|
@@ -13708,7 +13780,8 @@ var actionKindSchema = exports_external.union([
|
|
|
13708
13780
|
exports_external.literal("restart-app"),
|
|
13709
13781
|
exports_external.literal("background-app"),
|
|
13710
13782
|
exports_external.literal("open-url"),
|
|
13711
|
-
exports_external.literal("alert")
|
|
13783
|
+
exports_external.literal("alert"),
|
|
13784
|
+
exports_external.literal("key")
|
|
13712
13785
|
]);
|
|
13713
13786
|
var actionRequestSchema = exports_external.object({
|
|
13714
13787
|
kind: actionKindSchema,
|
|
@@ -13726,6 +13799,8 @@ var actionRequestSchema = exports_external.object({
|
|
|
13726
13799
|
url: exports_external.string().optional(),
|
|
13727
13800
|
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
|
|
13728
13801
|
seconds: exports_external.number().optional(),
|
|
13802
|
+
key: keyNameSchema.optional(),
|
|
13803
|
+
modifiers: exports_external.array(keyModifierSchema).optional(),
|
|
13729
13804
|
screenshot: exports_external.boolean().optional(),
|
|
13730
13805
|
settleMs: exports_external.number().int().min(0).max(1e4).optional(),
|
|
13731
13806
|
fullImage: exports_external.boolean().optional(),
|
|
@@ -13883,109 +13958,71 @@ function pickLatestRun(runs) {
|
|
|
13883
13958
|
return runs.reduce((best, run) => run.createdAt > best.createdAt ? run : best);
|
|
13884
13959
|
}
|
|
13885
13960
|
|
|
13886
|
-
//
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
String(Math.round(action.y2))
|
|
13938
|
-
];
|
|
13939
|
-
if (action.durationMs != null) {
|
|
13940
|
-
parts.push("--duration", String(Math.round(action.durationMs)));
|
|
13941
|
-
}
|
|
13942
|
-
return parts.join(" ");
|
|
13943
|
-
}
|
|
13944
|
-
if (action.type === "type") {
|
|
13945
|
-
return `yoqa action input --text ${shellSingleQuote(action.text)}`;
|
|
13946
|
-
}
|
|
13947
|
-
if (action.type === "assert") {
|
|
13948
|
-
const timeoutSeconds = action.timeoutMs != null ? Math.max(1, Math.round(action.timeoutMs / 1000)) : undefined;
|
|
13949
|
-
const parts = ["yoqa", "assert", action.assertion, "--text", shellSingleQuote(action.text)];
|
|
13950
|
-
if (timeoutSeconds != null) {
|
|
13951
|
-
parts.push("--timeout", String(timeoutSeconds));
|
|
13952
|
-
}
|
|
13953
|
-
return parts.join(" ");
|
|
13954
|
-
}
|
|
13955
|
-
if (action.type === "alert") {
|
|
13956
|
-
return action.alertAction === "dismiss" ? "yoqa action alert --dismiss" : "yoqa action alert";
|
|
13957
|
-
}
|
|
13958
|
-
if (action.type === "activate-app" || action.type === "terminate-app" || action.type === "restart-app") {
|
|
13959
|
-
return `yoqa action ${action.type} --app-id ${shellSingleQuote(action.appId)}`;
|
|
13960
|
-
}
|
|
13961
|
-
if (action.type === "background-app") {
|
|
13962
|
-
return action.seconds != null ? `yoqa action background-app --seconds ${action.seconds}` : "yoqa action background-app";
|
|
13963
|
-
}
|
|
13964
|
-
if (action.type === "open-url") {
|
|
13965
|
-
return `yoqa action open-url --url ${shellSingleQuote(action.url)}`;
|
|
13961
|
+
// ../../node_modules/yaml/dist/index.js
|
|
13962
|
+
var composer = require_composer();
|
|
13963
|
+
var Document = require_Document();
|
|
13964
|
+
var Schema = require_Schema();
|
|
13965
|
+
var errors2 = require_errors();
|
|
13966
|
+
var Alias = require_Alias();
|
|
13967
|
+
var identity = require_identity();
|
|
13968
|
+
var Pair = require_Pair();
|
|
13969
|
+
var Scalar = require_Scalar();
|
|
13970
|
+
var YAMLMap = require_YAMLMap();
|
|
13971
|
+
var YAMLSeq = require_YAMLSeq();
|
|
13972
|
+
var cst = require_cst();
|
|
13973
|
+
var lexer = require_lexer();
|
|
13974
|
+
var lineCounter = require_line_counter();
|
|
13975
|
+
var parser = require_parser();
|
|
13976
|
+
var publicApi = require_public_api();
|
|
13977
|
+
var visit = require_visit();
|
|
13978
|
+
var $Composer = composer.Composer;
|
|
13979
|
+
var $Document = Document.Document;
|
|
13980
|
+
var $Schema = Schema.Schema;
|
|
13981
|
+
var $YAMLError = errors2.YAMLError;
|
|
13982
|
+
var $YAMLParseError = errors2.YAMLParseError;
|
|
13983
|
+
var $YAMLWarning = errors2.YAMLWarning;
|
|
13984
|
+
var $Alias = Alias.Alias;
|
|
13985
|
+
var $isAlias = identity.isAlias;
|
|
13986
|
+
var $isCollection = identity.isCollection;
|
|
13987
|
+
var $isDocument = identity.isDocument;
|
|
13988
|
+
var $isMap = identity.isMap;
|
|
13989
|
+
var $isNode = identity.isNode;
|
|
13990
|
+
var $isPair = identity.isPair;
|
|
13991
|
+
var $isScalar = identity.isScalar;
|
|
13992
|
+
var $isSeq = identity.isSeq;
|
|
13993
|
+
var $Pair = Pair.Pair;
|
|
13994
|
+
var $Scalar = Scalar.Scalar;
|
|
13995
|
+
var $YAMLMap = YAMLMap.YAMLMap;
|
|
13996
|
+
var $YAMLSeq = YAMLSeq.YAMLSeq;
|
|
13997
|
+
var $Lexer = lexer.Lexer;
|
|
13998
|
+
var $LineCounter = lineCounter.LineCounter;
|
|
13999
|
+
var $Parser = parser.Parser;
|
|
14000
|
+
var $parse = publicApi.parse;
|
|
14001
|
+
var $parseAllDocuments = publicApi.parseAllDocuments;
|
|
14002
|
+
var $parseDocument = publicApi.parseDocument;
|
|
14003
|
+
var $stringify = publicApi.stringify;
|
|
14004
|
+
var $visit = visit.visit;
|
|
14005
|
+
var $visitAsync = visit.visitAsync;
|
|
14006
|
+
|
|
14007
|
+
// ../runner-client/src/keys.ts
|
|
14008
|
+
function byName(names, raw, what) {
|
|
14009
|
+
const match = names.find((name) => name.toLowerCase() === raw.trim().toLowerCase());
|
|
14010
|
+
if (!match) {
|
|
14011
|
+
throw new Error(`Unknown ${what} "${raw}" (expected one of: ${names.join(", ")})`);
|
|
13966
14012
|
}
|
|
13967
|
-
|
|
13968
|
-
return `sleep ${seconds}`;
|
|
14013
|
+
return match;
|
|
13969
14014
|
}
|
|
13970
|
-
function
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
"# Requires an active device session: yoqa devices connect <id>",
|
|
13980
|
-
"# Prefer the Case Script file: `yoqa script run <file.yoqa.yaml>`.",
|
|
13981
|
-
"set -euo pipefail",
|
|
13982
|
-
"",
|
|
13983
|
-
...script.actions.map(actionToShellLine),
|
|
13984
|
-
""
|
|
13985
|
-
];
|
|
13986
|
-
return lines.join(`
|
|
13987
|
-
`);
|
|
14015
|
+
function parseKeyName(raw) {
|
|
14016
|
+
return byName(KEY_NAMES, raw, "key");
|
|
14017
|
+
}
|
|
14018
|
+
function parseKeyModifiers(raw) {
|
|
14019
|
+
const names = raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
14020
|
+
return [...new Set(names.map((name) => byName(KEY_MODIFIERS, name, "modifier")))];
|
|
14021
|
+
}
|
|
14022
|
+
function formatKeyChord(key, modifiers = []) {
|
|
14023
|
+
return [...modifiers, key].join("+");
|
|
13988
14024
|
}
|
|
14025
|
+
|
|
13989
14026
|
// ../runner-client/src/xml-entities.ts
|
|
13990
14027
|
var NAMED = {
|
|
13991
14028
|
amp: "&",
|
|
@@ -14012,303 +14049,66 @@ function decodeXmlEntities(value) {
|
|
|
14012
14049
|
}
|
|
14013
14050
|
|
|
14014
14051
|
// ../runner-client/src/shell-script.ts
|
|
14015
|
-
function
|
|
14052
|
+
function shellSingleQuote(value) {
|
|
14016
14053
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14017
14054
|
}
|
|
14018
|
-
function
|
|
14019
|
-
const
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
if (quote === "'") {
|
|
14027
|
-
if (ch === "'") {
|
|
14028
|
-
quote = null;
|
|
14029
|
-
} else {
|
|
14030
|
-
current += ch;
|
|
14031
|
-
}
|
|
14032
|
-
continue;
|
|
14033
|
-
}
|
|
14034
|
-
if (quote === '"') {
|
|
14035
|
-
if (ch === "\\") {
|
|
14036
|
-
const next = line[i + 1];
|
|
14037
|
-
if (next != null) {
|
|
14038
|
-
current += next;
|
|
14039
|
-
i++;
|
|
14040
|
-
}
|
|
14041
|
-
continue;
|
|
14042
|
-
}
|
|
14043
|
-
if (ch === '"') {
|
|
14044
|
-
quote = null;
|
|
14045
|
-
} else {
|
|
14046
|
-
current += ch;
|
|
14047
|
-
}
|
|
14048
|
-
continue;
|
|
14049
|
-
}
|
|
14050
|
-
if (ch === "'" || ch === '"') {
|
|
14051
|
-
quote = ch;
|
|
14052
|
-
continue;
|
|
14053
|
-
}
|
|
14054
|
-
if (/\s/.test(ch)) {
|
|
14055
|
-
if (current.length > 0) {
|
|
14056
|
-
tokens.push(current);
|
|
14057
|
-
current = "";
|
|
14058
|
-
}
|
|
14059
|
-
continue;
|
|
14060
|
-
}
|
|
14061
|
-
current += ch;
|
|
14062
|
-
}
|
|
14063
|
-
if (quote != null) {
|
|
14064
|
-
throw new Error("Unclosed quote");
|
|
14055
|
+
function formatActionShellLine(action) {
|
|
14056
|
+
const parts = ["yoqa", "action", action.kind];
|
|
14057
|
+
if (action.kind === "key" && action.key)
|
|
14058
|
+
parts.push(action.key);
|
|
14059
|
+
if (action.modifiers?.length)
|
|
14060
|
+
parts.push("--modifiers", action.modifiers.join(","));
|
|
14061
|
+
if (action.id) {
|
|
14062
|
+
parts.push("--id", shellSingleQuote(action.id));
|
|
14065
14063
|
}
|
|
14066
|
-
if (
|
|
14067
|
-
|
|
14064
|
+
if (action.label) {
|
|
14065
|
+
parts.push("--label", shellSingleQuote(action.label));
|
|
14068
14066
|
}
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
function parseFlagMap(tokens) {
|
|
14072
|
-
const flags = new Map;
|
|
14073
|
-
for (let i = 0;i < tokens.length; i++) {
|
|
14074
|
-
const token = tokens[i];
|
|
14075
|
-
if (token == null || !token.startsWith("-"))
|
|
14076
|
-
continue;
|
|
14077
|
-
const eq = token.indexOf("=");
|
|
14078
|
-
if (eq > 0) {
|
|
14079
|
-
flags.set(token.slice(0, eq), token.slice(eq + 1));
|
|
14080
|
-
continue;
|
|
14081
|
-
}
|
|
14082
|
-
const next = tokens[i + 1];
|
|
14083
|
-
if (next != null && !next.startsWith("-")) {
|
|
14084
|
-
flags.set(token, next);
|
|
14085
|
-
i++;
|
|
14086
|
-
} else {
|
|
14087
|
-
flags.set(token, true);
|
|
14088
|
-
}
|
|
14067
|
+
if (action.description) {
|
|
14068
|
+
parts.push("-d", shellSingleQuote(action.description));
|
|
14089
14069
|
}
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14070
|
+
if (action.x != null)
|
|
14071
|
+
parts.push("--x", String(Math.round(action.x)));
|
|
14072
|
+
if (action.y != null)
|
|
14073
|
+
parts.push("--y", String(Math.round(action.y)));
|
|
14074
|
+
if (action.x2 != null)
|
|
14075
|
+
parts.push("--x2", String(Math.round(action.x2)));
|
|
14076
|
+
if (action.y2 != null)
|
|
14077
|
+
parts.push("--y2", String(Math.round(action.y2)));
|
|
14078
|
+
if (action.durationMs != null)
|
|
14079
|
+
parts.push("--duration", String(Math.round(action.durationMs)));
|
|
14080
|
+
if (action.double)
|
|
14081
|
+
parts.push("--double");
|
|
14082
|
+
if (action.text != null)
|
|
14083
|
+
parts.push("--text", shellSingleQuote(action.text));
|
|
14084
|
+
if (action.appId != null)
|
|
14085
|
+
parts.push("--app-id", shellSingleQuote(action.appId));
|
|
14086
|
+
if (action.url != null)
|
|
14087
|
+
parts.push("--url", shellSingleQuote(action.url));
|
|
14088
|
+
if (action.seconds != null)
|
|
14089
|
+
parts.push("--seconds", String(action.seconds));
|
|
14090
|
+
if (action.kind === "alert") {
|
|
14091
|
+
if (action.alertAction === "dismiss")
|
|
14092
|
+
parts.push("--dismiss");
|
|
14097
14093
|
}
|
|
14098
|
-
return;
|
|
14094
|
+
return parts.join(" ");
|
|
14099
14095
|
}
|
|
14100
|
-
function
|
|
14101
|
-
const
|
|
14102
|
-
|
|
14103
|
-
|
|
14104
|
-
const n = Number(raw);
|
|
14105
|
-
return Number.isFinite(n) ? n : undefined;
|
|
14096
|
+
function formatSleepShellLine(seconds) {
|
|
14097
|
+
const safe = Math.max(0, seconds);
|
|
14098
|
+
const rounded = Number.isInteger(safe) ? String(safe) : String(Number(safe.toFixed(3)));
|
|
14099
|
+
return `sleep ${rounded}`;
|
|
14106
14100
|
}
|
|
14107
|
-
function
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
|
|
14111
|
-
|
|
14112
|
-
if (!kindRaw) {
|
|
14113
|
-
throw new Error("Missing action kind");
|
|
14114
|
-
}
|
|
14115
|
-
const kindParsed = actionKindSchema.safeParse(kindRaw);
|
|
14116
|
-
if (!kindParsed.success) {
|
|
14117
|
-
throw new Error(`Unknown action kind: ${kindRaw}`);
|
|
14118
|
-
}
|
|
14119
|
-
const kind = kindParsed.data;
|
|
14120
|
-
const flags = parseFlagMap(tokens.slice(3));
|
|
14121
|
-
const action = { kind };
|
|
14122
|
-
const description = flagString(flags, "-d", "--description");
|
|
14123
|
-
if (description != null)
|
|
14124
|
-
action.description = description;
|
|
14125
|
-
const label = flagString(flags, "--label");
|
|
14126
|
-
if (label != null)
|
|
14127
|
-
action.label = label;
|
|
14128
|
-
const id = flagString(flags, "--id");
|
|
14129
|
-
if (id != null)
|
|
14130
|
-
action.id = id;
|
|
14131
|
-
const x = flagNumber(flags, "--x");
|
|
14132
|
-
const y = flagNumber(flags, "--y");
|
|
14133
|
-
const x2 = flagNumber(flags, "--x2");
|
|
14134
|
-
const y2 = flagNumber(flags, "--y2");
|
|
14135
|
-
const durationMs = flagNumber(flags, "--duration");
|
|
14136
|
-
const text = flagString(flags, "--text");
|
|
14137
|
-
const appId = flagString(flags, "--app-id", "--bundle-id");
|
|
14138
|
-
const url = flagString(flags, "--url");
|
|
14139
|
-
const seconds = flagNumber(flags, "--seconds");
|
|
14140
|
-
if (x != null)
|
|
14141
|
-
action.x = x;
|
|
14142
|
-
if (y != null)
|
|
14143
|
-
action.y = y;
|
|
14144
|
-
if (x2 != null)
|
|
14145
|
-
action.x2 = x2;
|
|
14146
|
-
if (y2 != null)
|
|
14147
|
-
action.y2 = y2;
|
|
14148
|
-
if (durationMs != null)
|
|
14149
|
-
action.durationMs = durationMs;
|
|
14150
|
-
if (text != null)
|
|
14151
|
-
action.text = text;
|
|
14152
|
-
if (appId != null)
|
|
14153
|
-
action.appId = appId;
|
|
14154
|
-
if (url != null)
|
|
14155
|
-
action.url = url;
|
|
14156
|
-
if (seconds != null)
|
|
14157
|
-
action.seconds = seconds;
|
|
14158
|
-
if (flags.has("--double"))
|
|
14159
|
-
action.double = true;
|
|
14160
|
-
if (flags.has("--dismiss")) {
|
|
14161
|
-
action.alertAction = "dismiss";
|
|
14162
|
-
} else if (flags.has("--accept") || kind === "alert") {
|
|
14163
|
-
const alertAction = flagString(flags, "--action");
|
|
14164
|
-
if (alertAction === "dismiss")
|
|
14165
|
-
action.alertAction = "dismiss";
|
|
14166
|
-
else if (alertAction === "accept" || kind === "alert")
|
|
14167
|
-
action.alertAction = "accept";
|
|
14168
|
-
}
|
|
14169
|
-
return { kind: "action", action, lineNumber, raw };
|
|
14101
|
+
function assertTargetFlags(target) {
|
|
14102
|
+
return [
|
|
14103
|
+
...target.text != null ? ["--text", shellSingleQuote(target.text.trim())] : [],
|
|
14104
|
+
...target.id != null ? ["--id", shellSingleQuote(target.id.trim())] : []
|
|
14105
|
+
];
|
|
14170
14106
|
}
|
|
14171
|
-
function
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
if (pathToken?.startsWith("-")) {
|
|
14177
|
-
throw new Error("`yoqa screenshot` accepts an optional output path only");
|
|
14178
|
-
}
|
|
14179
|
-
if (tokens.length > 3) {
|
|
14180
|
-
throw new Error("`yoqa screenshot` accepts at most one path argument");
|
|
14181
|
-
}
|
|
14182
|
-
const path = pathToken?.trim() ? pathToken.trim() : null;
|
|
14183
|
-
return { kind: "screenshot", path, lineNumber, raw };
|
|
14184
|
-
}
|
|
14185
|
-
function parseAssertFromTokens(tokens, lineNumber, raw) {
|
|
14186
|
-
if (tokens[0] !== "yoqa" || tokens[1] !== "assert") {
|
|
14187
|
-
throw new Error("Expected `yoqa assert <visible|not-visible> …`");
|
|
14188
|
-
}
|
|
14189
|
-
const assertionRaw = tokens[2];
|
|
14190
|
-
if (assertionRaw !== "visible" && assertionRaw !== "not-visible") {
|
|
14191
|
-
throw new Error("Assert kind must be `visible` or `not-visible`");
|
|
14192
|
-
}
|
|
14193
|
-
const flags = parseFlagMap(tokens.slice(3));
|
|
14194
|
-
const text = flagString(flags, "--text", "-t");
|
|
14195
|
-
if (!text || text.trim().length === 0) {
|
|
14196
|
-
throw new Error("assert requires --text");
|
|
14197
|
-
}
|
|
14198
|
-
const timeout = flagNumber(flags, "--timeout") ?? 5;
|
|
14199
|
-
if (timeout < 0) {
|
|
14200
|
-
throw new Error("--timeout must be non-negative");
|
|
14201
|
-
}
|
|
14202
|
-
return {
|
|
14203
|
-
kind: "assert",
|
|
14204
|
-
assertion: assertionRaw,
|
|
14205
|
-
text: text.trim(),
|
|
14206
|
-
timeoutSeconds: timeout,
|
|
14207
|
-
lineNumber,
|
|
14208
|
-
raw
|
|
14209
|
-
};
|
|
14210
|
-
}
|
|
14211
|
-
function isIgnorableLine(trimmed) {
|
|
14212
|
-
if (trimmed.length === 0)
|
|
14213
|
-
return true;
|
|
14214
|
-
if (trimmed.startsWith("#"))
|
|
14215
|
-
return true;
|
|
14216
|
-
if (trimmed === "set -euo pipefail" || trimmed === "set -e" || trimmed === "set -eu")
|
|
14217
|
-
return true;
|
|
14218
|
-
return false;
|
|
14219
|
-
}
|
|
14220
|
-
function parseYoqaShellScript(text) {
|
|
14221
|
-
const lines = text.split(/\r?\n/);
|
|
14222
|
-
const steps = [];
|
|
14223
|
-
const errors2 = [];
|
|
14224
|
-
for (let i = 0;i < lines.length; i++) {
|
|
14225
|
-
const lineNumber = i + 1;
|
|
14226
|
-
const raw = lines[i] ?? "";
|
|
14227
|
-
const trimmed = raw.trim();
|
|
14228
|
-
if (isIgnorableLine(trimmed))
|
|
14229
|
-
continue;
|
|
14230
|
-
try {
|
|
14231
|
-
const tokens = tokenizeShellLine(trimmed);
|
|
14232
|
-
if (tokens.length === 0)
|
|
14233
|
-
continue;
|
|
14234
|
-
if (tokens[0] === "sleep") {
|
|
14235
|
-
const seconds = Number(tokens[1]);
|
|
14236
|
-
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
14237
|
-
throw new Error("`sleep` requires a non-negative number of seconds");
|
|
14238
|
-
}
|
|
14239
|
-
steps.push({ kind: "sleep", seconds, lineNumber, raw: trimmed });
|
|
14240
|
-
continue;
|
|
14241
|
-
}
|
|
14242
|
-
if (tokens[0] === "yoqa" && tokens[1] === "assert") {
|
|
14243
|
-
steps.push(parseAssertFromTokens(tokens, lineNumber, trimmed));
|
|
14244
|
-
continue;
|
|
14245
|
-
}
|
|
14246
|
-
if (tokens[0] === "yoqa" && tokens[1] === "screenshot") {
|
|
14247
|
-
steps.push(parseScreenshotFromTokens(tokens, lineNumber, trimmed));
|
|
14248
|
-
continue;
|
|
14249
|
-
}
|
|
14250
|
-
if (tokens[0] === "yoqa") {
|
|
14251
|
-
steps.push(parseActionFromTokens(tokens, lineNumber, trimmed));
|
|
14252
|
-
continue;
|
|
14253
|
-
}
|
|
14254
|
-
throw new Error(`Unsupported command: ${tokens[0]}`);
|
|
14255
|
-
} catch (error) {
|
|
14256
|
-
errors2.push({
|
|
14257
|
-
lineNumber,
|
|
14258
|
-
raw: trimmed,
|
|
14259
|
-
message: error instanceof Error ? error.message : String(error)
|
|
14260
|
-
});
|
|
14261
|
-
}
|
|
14262
|
-
}
|
|
14263
|
-
return { steps, errors: errors2 };
|
|
14264
|
-
}
|
|
14265
|
-
function formatActionShellLine(action) {
|
|
14266
|
-
const parts = ["yoqa", "action", action.kind];
|
|
14267
|
-
if (action.id) {
|
|
14268
|
-
parts.push("--id", shellSingleQuote2(action.id));
|
|
14269
|
-
}
|
|
14270
|
-
if (action.label) {
|
|
14271
|
-
parts.push("--label", shellSingleQuote2(action.label));
|
|
14272
|
-
}
|
|
14273
|
-
if (action.description) {
|
|
14274
|
-
parts.push("-d", shellSingleQuote2(action.description));
|
|
14275
|
-
}
|
|
14276
|
-
if (action.x != null)
|
|
14277
|
-
parts.push("--x", String(Math.round(action.x)));
|
|
14278
|
-
if (action.y != null)
|
|
14279
|
-
parts.push("--y", String(Math.round(action.y)));
|
|
14280
|
-
if (action.x2 != null)
|
|
14281
|
-
parts.push("--x2", String(Math.round(action.x2)));
|
|
14282
|
-
if (action.y2 != null)
|
|
14283
|
-
parts.push("--y2", String(Math.round(action.y2)));
|
|
14284
|
-
if (action.durationMs != null)
|
|
14285
|
-
parts.push("--duration", String(Math.round(action.durationMs)));
|
|
14286
|
-
if (action.double)
|
|
14287
|
-
parts.push("--double");
|
|
14288
|
-
if (action.text != null)
|
|
14289
|
-
parts.push("--text", shellSingleQuote2(action.text));
|
|
14290
|
-
if (action.appId != null)
|
|
14291
|
-
parts.push("--app-id", shellSingleQuote2(action.appId));
|
|
14292
|
-
if (action.url != null)
|
|
14293
|
-
parts.push("--url", shellSingleQuote2(action.url));
|
|
14294
|
-
if (action.seconds != null)
|
|
14295
|
-
parts.push("--seconds", String(action.seconds));
|
|
14296
|
-
if (action.kind === "alert") {
|
|
14297
|
-
if (action.alertAction === "dismiss")
|
|
14298
|
-
parts.push("--dismiss");
|
|
14299
|
-
}
|
|
14300
|
-
return parts.join(" ");
|
|
14301
|
-
}
|
|
14302
|
-
function formatSleepShellLine(seconds) {
|
|
14303
|
-
const safe = Math.max(0, seconds);
|
|
14304
|
-
const rounded = Number.isInteger(safe) ? String(safe) : String(Number(safe.toFixed(3)));
|
|
14305
|
-
return `sleep ${rounded}`;
|
|
14306
|
-
}
|
|
14307
|
-
function formatAssertShellLine(input) {
|
|
14308
|
-
const parts = ["yoqa", "assert", input.assertion, "--text", shellSingleQuote2(input.text.trim())];
|
|
14309
|
-
const timeout = input.timeoutSeconds ?? 5;
|
|
14310
|
-
if (timeout !== 5) {
|
|
14311
|
-
parts.push("--timeout", String(timeout));
|
|
14107
|
+
function formatAssertShellLine(input) {
|
|
14108
|
+
const parts = ["yoqa", "assert", input.assertion, ...assertTargetFlags(input)];
|
|
14109
|
+
const timeout = input.timeoutSeconds ?? 5;
|
|
14110
|
+
if (timeout !== 5) {
|
|
14111
|
+
parts.push("--timeout", String(timeout));
|
|
14312
14112
|
}
|
|
14313
14113
|
return parts.join(" ");
|
|
14314
14114
|
}
|
|
@@ -14320,25 +14120,6 @@ var DEFAULT_SHELL_SCRIPT_HEADER = [
|
|
|
14320
14120
|
""
|
|
14321
14121
|
].join(`
|
|
14322
14122
|
`);
|
|
14323
|
-
function sleepMs(ms, signal) {
|
|
14324
|
-
if (ms <= 0)
|
|
14325
|
-
return Promise.resolve();
|
|
14326
|
-
return new Promise((resolve, reject) => {
|
|
14327
|
-
if (signal?.aborted) {
|
|
14328
|
-
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
14329
|
-
return;
|
|
14330
|
-
}
|
|
14331
|
-
const timer = setTimeout(() => {
|
|
14332
|
-
signal?.removeEventListener("abort", onAbort);
|
|
14333
|
-
resolve();
|
|
14334
|
-
}, ms);
|
|
14335
|
-
const onAbort = () => {
|
|
14336
|
-
clearTimeout(timer);
|
|
14337
|
-
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
14338
|
-
};
|
|
14339
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
14340
|
-
});
|
|
14341
|
-
}
|
|
14342
14123
|
function normalizedLabel(value) {
|
|
14343
14124
|
return decodeXmlEntities(value ?? "").trim().toLowerCase();
|
|
14344
14125
|
}
|
|
@@ -14352,1156 +14133,1495 @@ function screenHasText(elements, text) {
|
|
|
14352
14133
|
return label.includes(needle) || type.includes(needle);
|
|
14353
14134
|
});
|
|
14354
14135
|
}
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
throw Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
14360
|
-
}
|
|
14361
|
-
const screen = await client.getScreen();
|
|
14362
|
-
const found = screenHasText(screen.elements, step.text);
|
|
14363
|
-
if (step.assertion === "visible" && found)
|
|
14364
|
-
return;
|
|
14365
|
-
if (step.assertion === "not-visible" && !found)
|
|
14366
|
-
return;
|
|
14367
|
-
if (Date.now() >= deadline) {
|
|
14368
|
-
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}`);
|
|
14369
|
-
}
|
|
14370
|
-
await sleepMs(400, signal);
|
|
14371
|
-
}
|
|
14136
|
+
function screenHasTarget(elements, target) {
|
|
14137
|
+
if (target.id != null)
|
|
14138
|
+
return findElementById(elements, target.id) != null;
|
|
14139
|
+
return screenHasText(elements, target.text ?? "");
|
|
14372
14140
|
}
|
|
14373
|
-
|
|
14374
|
-
const
|
|
14375
|
-
if (
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
};
|
|
14386
|
-
}
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14141
|
+
function findElementById(elements, id) {
|
|
14142
|
+
const needle = id.trim().toLowerCase();
|
|
14143
|
+
if (!needle)
|
|
14144
|
+
return null;
|
|
14145
|
+
const list = elements ?? [];
|
|
14146
|
+
const exact = list.filter((el) => (el.id?.trim().toLowerCase() ?? "") === needle);
|
|
14147
|
+
if (exact.length > 0) {
|
|
14148
|
+
exact.sort((a, b) => a.width * a.height - b.width * b.height);
|
|
14149
|
+
return exact[0] ?? null;
|
|
14150
|
+
}
|
|
14151
|
+
const partial = list.filter((el) => {
|
|
14152
|
+
const value = el.id?.toLowerCase() ?? "";
|
|
14153
|
+
return value === needle || value.endsWith(`/${needle}`) || value.endsWith(`:id/${needle}`);
|
|
14154
|
+
});
|
|
14155
|
+
if (partial.length === 0)
|
|
14156
|
+
return null;
|
|
14157
|
+
partial.sort((a, b) => a.width * a.height - b.width * b.height);
|
|
14158
|
+
return partial[0] ?? null;
|
|
14159
|
+
}
|
|
14160
|
+
|
|
14161
|
+
// ../runner-client/src/case-script-file.ts
|
|
14162
|
+
var ACTION_TYPES = Object.keys(caseScriptActionSchemaByType).filter((type) => type !== "branch");
|
|
14163
|
+
var BRANCH_KEY = "if";
|
|
14164
|
+
var ITEM_NAMES = [...ACTION_TYPES, BRANCH_KEY];
|
|
14165
|
+
var BRANCH_FIELDS = ["text", "label", "id", "assertion", "timeoutMs", "then", "else", "reason"];
|
|
14166
|
+
var SHORT_FORM_FIELD = {
|
|
14167
|
+
tap: "label",
|
|
14168
|
+
type: "text",
|
|
14169
|
+
wait: "ms",
|
|
14170
|
+
assert: "text",
|
|
14171
|
+
alert: "alertAction",
|
|
14172
|
+
"open-url": "url",
|
|
14173
|
+
"activate-app": "appId",
|
|
14174
|
+
"terminate-app": "appId",
|
|
14175
|
+
"restart-app": "appId",
|
|
14176
|
+
"background-app": "seconds",
|
|
14177
|
+
key: "key"
|
|
14178
|
+
};
|
|
14179
|
+
var TEXT_SHORT_FORM_FIELDS = new Set(["label", "text", "alertAction", "url", "appId", "key"]);
|
|
14180
|
+
var FIELD_ORDER = [
|
|
14181
|
+
"label",
|
|
14182
|
+
"id",
|
|
14183
|
+
"text",
|
|
14184
|
+
"appId",
|
|
14185
|
+
"url",
|
|
14186
|
+
"key",
|
|
14187
|
+
"modifiers",
|
|
14188
|
+
"assertion",
|
|
14189
|
+
"alertAction",
|
|
14190
|
+
"x",
|
|
14191
|
+
"y",
|
|
14192
|
+
"x2",
|
|
14193
|
+
"y2",
|
|
14194
|
+
"ms",
|
|
14195
|
+
"seconds",
|
|
14196
|
+
"timeoutMs",
|
|
14197
|
+
"double",
|
|
14198
|
+
"durationMs",
|
|
14199
|
+
"reason"
|
|
14200
|
+
];
|
|
14201
|
+
var TOP_LEVEL_KEYS = new Set(["version", "name", "sourceRunId", "savedAt", "actions"]);
|
|
14202
|
+
function isActionType(value) {
|
|
14203
|
+
return typeof value === "string" && ACTION_TYPES.includes(value);
|
|
14204
|
+
}
|
|
14205
|
+
function actionFieldNames(type) {
|
|
14206
|
+
const schema = caseScriptActionSchemaByType[type];
|
|
14207
|
+
const object = schema instanceof exports_external.ZodEffects ? schema.innerType() : schema;
|
|
14208
|
+
return new Set(Object.keys(object.shape).filter((key) => key !== "type"));
|
|
14209
|
+
}
|
|
14210
|
+
function lineOf(node, lineCounter2) {
|
|
14211
|
+
const start = node?.range?.[0];
|
|
14212
|
+
return start == null ? null : lineCounter2.linePos(start).line;
|
|
14213
|
+
}
|
|
14214
|
+
function zodMessage(error) {
|
|
14215
|
+
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
14216
|
+
}
|
|
14217
|
+
function readAction(raw, legacy) {
|
|
14218
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
14219
|
+
return { ok: false, message: "expected an action such as `- tap: Home`" };
|
|
14391
14220
|
}
|
|
14392
|
-
|
|
14393
|
-
|
|
14394
|
-
|
|
14221
|
+
const entries = Object.entries(raw);
|
|
14222
|
+
let type;
|
|
14223
|
+
let fields;
|
|
14224
|
+
if (legacy) {
|
|
14225
|
+
const declared = raw.type;
|
|
14226
|
+
if (!isActionType(declared)) {
|
|
14227
|
+
return { ok: false, message: `unknown action type ${JSON.stringify(declared)}` };
|
|
14395
14228
|
}
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
}
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
} else {
|
|
14408
|
-
await client.performAction(step.action);
|
|
14409
|
-
if (options.pauseAfterActionMs) {
|
|
14410
|
-
await sleepMs(options.pauseAfterActionMs, options.signal);
|
|
14411
|
-
}
|
|
14412
|
-
}
|
|
14413
|
-
await options.onStep?.({ index, total, step, status: "ok" });
|
|
14414
|
-
} catch (error) {
|
|
14415
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
14416
|
-
await options.onStep?.({ index, total, step, status: "error", error: message });
|
|
14229
|
+
type = declared;
|
|
14230
|
+
fields = Object.fromEntries(entries.filter(([key]) => key !== "type"));
|
|
14231
|
+
} else {
|
|
14232
|
+
if (entries.length !== 1) {
|
|
14233
|
+
return {
|
|
14234
|
+
ok: false,
|
|
14235
|
+
message: `an action has exactly one key (its name); found ${entries.map(([key2]) => key2).join(", ") || "none"}`
|
|
14236
|
+
};
|
|
14237
|
+
}
|
|
14238
|
+
const [key, value] = entries[0];
|
|
14239
|
+
if (!isActionType(key)) {
|
|
14417
14240
|
return {
|
|
14418
14241
|
ok: false,
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14242
|
+
message: `unknown action "${key}" (expected one of: ${ITEM_NAMES.join(", ")})`
|
|
14243
|
+
};
|
|
14244
|
+
}
|
|
14245
|
+
type = key;
|
|
14246
|
+
if (value == null) {
|
|
14247
|
+
fields = {};
|
|
14248
|
+
} else if (typeof value === "object" && !Array.isArray(value)) {
|
|
14249
|
+
fields = value;
|
|
14250
|
+
} else if (Array.isArray(value)) {
|
|
14251
|
+
return { ok: false, message: `${type}: expected a value or a map of fields, not a list` };
|
|
14252
|
+
} else {
|
|
14253
|
+
const field = SHORT_FORM_FIELD[type];
|
|
14254
|
+
if (!field) {
|
|
14255
|
+
return { ok: false, message: `${type} has no short form; write it as a map of fields` };
|
|
14256
|
+
}
|
|
14257
|
+
fields = {
|
|
14258
|
+
[field]: TEXT_SHORT_FORM_FIELDS.has(field) && typeof value !== "string" ? String(value) : value
|
|
14422
14259
|
};
|
|
14423
14260
|
}
|
|
14424
14261
|
}
|
|
14425
|
-
|
|
14262
|
+
const allowed = actionFieldNames(type);
|
|
14263
|
+
const unknown = Object.keys(fields).filter((key) => !allowed.has(key));
|
|
14264
|
+
if (unknown.length > 0) {
|
|
14265
|
+
return {
|
|
14266
|
+
ok: false,
|
|
14267
|
+
message: `${type}: unknown field ${unknown.map((key) => `"${key}"`).join(", ")} (allowed: ${[...allowed].join(", ")})`
|
|
14268
|
+
};
|
|
14269
|
+
}
|
|
14270
|
+
const parsed = caseScriptActionSchemaByType[type].safeParse({ type, ...fields });
|
|
14271
|
+
if (!parsed.success) {
|
|
14272
|
+
return { ok: false, message: `${type}: ${zodMessage(parsed.error)}` };
|
|
14273
|
+
}
|
|
14274
|
+
return { ok: true, action: parsed.data };
|
|
14426
14275
|
}
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
14276
|
+
function pushItemError(reader, line, at, message) {
|
|
14277
|
+
reader.errors.push({
|
|
14278
|
+
line,
|
|
14279
|
+
action: at.action,
|
|
14280
|
+
...at.nested.length > 0 ? { nested: at.nested } : {},
|
|
14281
|
+
message
|
|
14282
|
+
});
|
|
14283
|
+
}
|
|
14284
|
+
function branchValue(item, legacy) {
|
|
14285
|
+
if (!$isMap(item))
|
|
14434
14286
|
return null;
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
const
|
|
14438
|
-
|
|
14287
|
+
if (legacy)
|
|
14288
|
+
return item.get("type") === "branch" ? { value: item } : null;
|
|
14289
|
+
const only = item.items.length === 1 ? item.items[0] : undefined;
|
|
14290
|
+
const key = only && $isScalar(only.key) ? only.key.value : null;
|
|
14291
|
+
return key === BRANCH_KEY ? { value: only?.value ?? null } : null;
|
|
14292
|
+
}
|
|
14293
|
+
function readItem(reader, item, at) {
|
|
14294
|
+
const line = lineOf(item, reader.lineCounter);
|
|
14295
|
+
const branch = branchValue(item, reader.legacy);
|
|
14296
|
+
if (branch)
|
|
14297
|
+
return readBranch(reader, branch.value, line, at);
|
|
14298
|
+
const raw = $isNode(item) ? item.toJS(reader.doc) : item;
|
|
14299
|
+
const result = readAction(raw, reader.legacy);
|
|
14300
|
+
if (result.ok)
|
|
14301
|
+
return result.action;
|
|
14302
|
+
pushItemError(reader, line, at, result.message);
|
|
14303
|
+
return null;
|
|
14304
|
+
}
|
|
14305
|
+
function readBranch(reader, value, itemLine, at) {
|
|
14306
|
+
const name = reader.legacy ? "branch" : BRANCH_KEY;
|
|
14307
|
+
if (!$isMap(value)) {
|
|
14308
|
+
pushItemError(reader, itemLine, at, `${name} has no short form; write it as a map with \`text:\` and \`then:\``);
|
|
14439
14309
|
return null;
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14310
|
+
}
|
|
14311
|
+
let ok = true;
|
|
14312
|
+
const fail = (line, message) => {
|
|
14313
|
+
pushItemError(reader, line ?? itemLine, at, `${name}: ${message}`);
|
|
14314
|
+
ok = false;
|
|
14315
|
+
};
|
|
14316
|
+
const keyLine = (key) => {
|
|
14317
|
+
const pair = value.items.find((item) => $isScalar(item.key) && item.key.value === key);
|
|
14318
|
+
return lineOf(pair?.key, reader.lineCounter);
|
|
14319
|
+
};
|
|
14320
|
+
const field = (key) => {
|
|
14321
|
+
const node = value.get(key, true);
|
|
14322
|
+
return $isNode(node) ? node.toJS(reader.doc) : node;
|
|
14323
|
+
};
|
|
14324
|
+
const unknown = value.items.map((item) => $isScalar(item.key) ? item.key.value : item.key).filter((key) => !(reader.legacy && key === "type")).filter((key) => typeof key !== "string" || !BRANCH_FIELDS.includes(key));
|
|
14325
|
+
if (unknown.length > 0) {
|
|
14326
|
+
fail(keyLine(String(unknown[0])), `unknown field ${unknown.map((key) => `"${String(key)}"`).join(", ")} (allowed: ${BRANCH_FIELDS.join(", ")})`);
|
|
14327
|
+
}
|
|
14328
|
+
const text = field("text");
|
|
14329
|
+
const label = field("label");
|
|
14330
|
+
const id = field("id");
|
|
14331
|
+
if (text != null && label != null)
|
|
14332
|
+
fail(keyLine("label"), "write `text` or `label`, not both");
|
|
14333
|
+
const textTarget = text ?? label;
|
|
14334
|
+
if (textTarget != null && id != null)
|
|
14335
|
+
fail(keyLine("id"), "write `text` or `id`, not both");
|
|
14336
|
+
if (textTarget == null && id == null) {
|
|
14337
|
+
fail(itemLine, "needs `text:` or `id:` (what to look for on the Screen)");
|
|
14338
|
+
}
|
|
14339
|
+
const readSide = (side) => {
|
|
14340
|
+
const node = value.get(side, true);
|
|
14341
|
+
if (node == null || $isScalar(node) && node.value == null)
|
|
14342
|
+
return;
|
|
14343
|
+
if (!$isSeq(node)) {
|
|
14344
|
+
fail(keyLine(side), `${side} must be a list of actions (each item starts with \`- \`)`);
|
|
14345
|
+
return;
|
|
14447
14346
|
}
|
|
14347
|
+
const actions = [];
|
|
14348
|
+
node.items.forEach((item, index) => {
|
|
14349
|
+
const action = readItem(reader, item, {
|
|
14350
|
+
action: at.action,
|
|
14351
|
+
nested: [...at.nested, { side, action: index + 1 }]
|
|
14352
|
+
});
|
|
14353
|
+
if (action)
|
|
14354
|
+
actions.push(action);
|
|
14355
|
+
else
|
|
14356
|
+
ok = false;
|
|
14357
|
+
});
|
|
14358
|
+
return actions;
|
|
14448
14359
|
};
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
if (type === "tap") {
|
|
14453
|
-
if (typeof record.label === "string" && record.label.trim())
|
|
14454
|
-
return `Tap: ${record.label}`;
|
|
14455
|
-
if (typeof record.id === "string" && record.id.trim())
|
|
14456
|
-
return `Tap #${record.id}`;
|
|
14457
|
-
return "Tap";
|
|
14360
|
+
const then = readSide("then");
|
|
14361
|
+
if (ok && (then == null || then.length === 0)) {
|
|
14362
|
+
fail(then == null ? itemLine : keyLine("then"), "needs `then:` with at least one action");
|
|
14458
14363
|
}
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
14364
|
+
const otherwise = readSide("else");
|
|
14365
|
+
if (!ok)
|
|
14366
|
+
return null;
|
|
14367
|
+
const fields = Object.fromEntries(["assertion", "timeoutMs", "reason"].map((key) => [key, field(key)]).filter(([, fieldValue]) => fieldValue !== undefined));
|
|
14368
|
+
const parsed = caseScriptActionSchemaByType.branch.safeParse({
|
|
14369
|
+
type: "branch",
|
|
14370
|
+
...id != null ? { id: String(id) } : { text: String(textTarget) },
|
|
14371
|
+
...fields,
|
|
14372
|
+
then,
|
|
14373
|
+
...otherwise ? { else: otherwise } : {}
|
|
14374
|
+
});
|
|
14375
|
+
if (!parsed.success) {
|
|
14376
|
+
pushItemError(reader, itemLine, at, `${name}: ${zodMessage(parsed.error)}`);
|
|
14377
|
+
return null;
|
|
14465
14378
|
}
|
|
14466
|
-
|
|
14467
|
-
|
|
14468
|
-
|
|
14469
|
-
|
|
14379
|
+
const { else: elseActions, ...branch } = parsed.data;
|
|
14380
|
+
return elseActions ? { ...branch, else: elseActions } : branch;
|
|
14381
|
+
}
|
|
14382
|
+
function documentErrors(doc, lineCounter2) {
|
|
14383
|
+
return doc.errors.map((error) => ({
|
|
14384
|
+
line: lineCounter2.linePos(error.pos[0]).line,
|
|
14385
|
+
action: null,
|
|
14386
|
+
message: error.message
|
|
14387
|
+
}));
|
|
14388
|
+
}
|
|
14389
|
+
function parseCaseScriptFile(text, options = {}) {
|
|
14390
|
+
const lineCounter2 = new $LineCounter;
|
|
14391
|
+
const doc = $parseDocument(text, { lineCounter: lineCounter2, prettyErrors: false });
|
|
14392
|
+
if (doc.errors.length > 0) {
|
|
14393
|
+
return { ok: false, errors: documentErrors(doc, lineCounter2) };
|
|
14470
14394
|
}
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14395
|
+
const root = doc.contents;
|
|
14396
|
+
if (!$isMap(root)) {
|
|
14397
|
+
return {
|
|
14398
|
+
ok: false,
|
|
14399
|
+
errors: [
|
|
14400
|
+
{
|
|
14401
|
+
line: lineOf(root, lineCounter2) ?? 1,
|
|
14402
|
+
action: null,
|
|
14403
|
+
message: "expected a Case Script with `version: 1` and `actions:`"
|
|
14404
|
+
}
|
|
14405
|
+
]
|
|
14406
|
+
};
|
|
14407
|
+
}
|
|
14408
|
+
const errors3 = [];
|
|
14409
|
+
const top = root.toJS(doc);
|
|
14410
|
+
for (const pair of root.items) {
|
|
14411
|
+
const key = $isScalar(pair.key) ? pair.key.value : pair.key;
|
|
14412
|
+
if (typeof key !== "string" || !TOP_LEVEL_KEYS.has(key)) {
|
|
14413
|
+
errors3.push({
|
|
14414
|
+
line: lineOf(pair.key, lineCounter2),
|
|
14415
|
+
action: null,
|
|
14416
|
+
message: `unknown key "${String(key)}" (allowed: ${[...TOP_LEVEL_KEYS].join(", ")})`
|
|
14417
|
+
});
|
|
14480
14418
|
}
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14419
|
+
}
|
|
14420
|
+
const versionNode = root.get("version", true);
|
|
14421
|
+
if (top.version !== 1) {
|
|
14422
|
+
errors3.push({
|
|
14423
|
+
line: lineOf(versionNode, lineCounter2) ?? 1,
|
|
14424
|
+
action: null,
|
|
14425
|
+
message: top.version === undefined ? "missing `version: 1`" : `unsupported version ${JSON.stringify(top.version)} (expected 1)`
|
|
14426
|
+
});
|
|
14427
|
+
}
|
|
14428
|
+
const name = top.name;
|
|
14429
|
+
if (name != null && typeof name !== "string") {
|
|
14430
|
+
errors3.push({
|
|
14431
|
+
line: lineOf(root.get("name", true), lineCounter2),
|
|
14432
|
+
action: null,
|
|
14433
|
+
message: "name must be text"
|
|
14434
|
+
});
|
|
14435
|
+
}
|
|
14436
|
+
const sourceRunId = top.sourceRunId;
|
|
14437
|
+
if (sourceRunId != null && (typeof sourceRunId !== "string" || sourceRunId.length === 0)) {
|
|
14438
|
+
errors3.push({
|
|
14439
|
+
line: lineOf(root.get("sourceRunId", true), lineCounter2),
|
|
14440
|
+
action: null,
|
|
14441
|
+
message: "sourceRunId must be text"
|
|
14442
|
+
});
|
|
14443
|
+
}
|
|
14444
|
+
const savedAt = top.savedAt;
|
|
14445
|
+
const legacy = savedAt !== undefined;
|
|
14446
|
+
if (legacy && (typeof savedAt !== "number" || !Number.isInteger(savedAt) || savedAt < 0)) {
|
|
14447
|
+
errors3.push({
|
|
14448
|
+
line: lineOf(root.get("savedAt", true), lineCounter2),
|
|
14449
|
+
action: null,
|
|
14450
|
+
message: "savedAt must be a non-negative whole number"
|
|
14451
|
+
});
|
|
14452
|
+
}
|
|
14453
|
+
const actionsNode = root.get("actions", true);
|
|
14454
|
+
const actions = [];
|
|
14455
|
+
const actionLines = [];
|
|
14456
|
+
if (actionsNode != null && !($isScalar(actionsNode) && actionsNode.value == null)) {
|
|
14457
|
+
if (!$isSeq(actionsNode)) {
|
|
14458
|
+
errors3.push({
|
|
14459
|
+
line: lineOf(actionsNode, lineCounter2),
|
|
14460
|
+
action: null,
|
|
14461
|
+
message: "actions must be a list (each item starts with `- `)"
|
|
14462
|
+
});
|
|
14463
|
+
} else {
|
|
14464
|
+
const reader = { doc, lineCounter: lineCounter2, legacy, errors: errors3 };
|
|
14465
|
+
actionsNode.items.forEach((item, index) => {
|
|
14466
|
+
const action = readItem(reader, item, { action: index + 1, nested: [] });
|
|
14467
|
+
if (action) {
|
|
14468
|
+
actions.push(action);
|
|
14469
|
+
actionLines.push(lineOf(item, lineCounter2) ?? 0);
|
|
14470
|
+
}
|
|
14471
|
+
});
|
|
14499
14472
|
}
|
|
14500
|
-
|
|
14473
|
+
} else if (!root.has("actions")) {
|
|
14474
|
+
errors3.push({ line: null, action: null, message: "missing `actions:`" });
|
|
14501
14475
|
}
|
|
14502
|
-
if (
|
|
14503
|
-
|
|
14476
|
+
if (errors3.length === 0 && actions.length === 0 && !options.allowEmpty) {
|
|
14477
|
+
errors3.push({
|
|
14478
|
+
line: lineOf(actionsNode, lineCounter2),
|
|
14479
|
+
action: null,
|
|
14480
|
+
message: "actions: add at least one action"
|
|
14481
|
+
});
|
|
14504
14482
|
}
|
|
14505
|
-
if (
|
|
14506
|
-
return
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
return "Terminate app";
|
|
14511
|
-
if (type === "restart-app")
|
|
14512
|
-
return "Restart app";
|
|
14513
|
-
if (type === "background-app")
|
|
14514
|
-
return "Background app";
|
|
14515
|
-
if (type === "open-url") {
|
|
14516
|
-
return typeof record.url === "string" && record.url.trim() ? `Open URL: ${record.url}` : "Open URL";
|
|
14483
|
+
if (errors3.length > 0) {
|
|
14484
|
+
return {
|
|
14485
|
+
ok: false,
|
|
14486
|
+
errors: errors3.sort((a, b) => (a.line ?? Number.POSITIVE_INFINITY) - (b.line ?? Number.POSITIVE_INFINITY))
|
|
14487
|
+
};
|
|
14517
14488
|
}
|
|
14518
|
-
return
|
|
14489
|
+
return {
|
|
14490
|
+
ok: true,
|
|
14491
|
+
name: typeof name === "string" && name.trim().length > 0 ? name.trim() : null,
|
|
14492
|
+
actionLines,
|
|
14493
|
+
script: {
|
|
14494
|
+
version: 1,
|
|
14495
|
+
...typeof sourceRunId === "string" ? { sourceRunId } : {},
|
|
14496
|
+
savedAt: typeof savedAt === "number" ? savedAt : options.savedAt ?? Date.now(),
|
|
14497
|
+
actions
|
|
14498
|
+
}
|
|
14499
|
+
};
|
|
14519
14500
|
}
|
|
14520
|
-
function
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
return
|
|
14528
|
-
|
|
14501
|
+
function formatCaseScriptFileErrors(errors3) {
|
|
14502
|
+
return errors3.map((error) => {
|
|
14503
|
+
const nested = (error.nested ?? []).map((step) => ` → ${step.side} → action ${step.action}`).join("");
|
|
14504
|
+
const where = [
|
|
14505
|
+
error.line != null ? `Line ${error.line}` : null,
|
|
14506
|
+
error.action != null ? `action ${error.action}${nested}` : null
|
|
14507
|
+
].filter(Boolean).join(", ");
|
|
14508
|
+
return where ? `${where}: ${error.message}` : error.message;
|
|
14509
|
+
}).join(`
|
|
14510
|
+
`);
|
|
14529
14511
|
}
|
|
14530
|
-
|
|
14531
|
-
|
|
14512
|
+
var FLOW_OPTIONS = {
|
|
14513
|
+
collectionStyle: "flow",
|
|
14514
|
+
lineWidth: 0,
|
|
14515
|
+
blockQuote: false,
|
|
14516
|
+
flowCollectionPadding: false
|
|
14517
|
+
};
|
|
14518
|
+
function yamlScalar(value) {
|
|
14519
|
+
if (typeof value === "string" && /[\n\r\t]/.test(value))
|
|
14520
|
+
return JSON.stringify(value);
|
|
14521
|
+
return $stringify([value], FLOW_OPTIONS).trim().slice(1, -1);
|
|
14532
14522
|
}
|
|
14533
|
-
function
|
|
14534
|
-
|
|
14523
|
+
function orderedFields(action) {
|
|
14524
|
+
const fields = Object.entries(action).filter(([key, value]) => key !== "type" && value !== undefined);
|
|
14525
|
+
const meaningful = fields.filter(([key, value]) => !(action.type === "assert" && key === "assertion" && value === "visible") && !(action.type === "alert" && key === "alertAction" && value === "accept"));
|
|
14526
|
+
const rank = (key) => {
|
|
14527
|
+
const at = FIELD_ORDER.indexOf(key);
|
|
14528
|
+
return at < 0 ? FIELD_ORDER.length : at;
|
|
14529
|
+
};
|
|
14530
|
+
return meaningful.sort(([a], [b]) => rank(a) - rank(b));
|
|
14535
14531
|
}
|
|
14536
|
-
function
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
if (type === "
|
|
14548
|
-
const
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
return formatAssertShellLine({
|
|
14554
|
-
assertion,
|
|
14555
|
-
text,
|
|
14556
|
-
...timeoutMs != null ? { timeoutSeconds: timeoutMs / 1000 } : {}
|
|
14532
|
+
function branchFields(branch) {
|
|
14533
|
+
const fields = branch.id != null ? [["id", branch.id]] : [["text", branch.text]];
|
|
14534
|
+
if (branch.assertion === "not-visible")
|
|
14535
|
+
fields.push(["assertion", branch.assertion]);
|
|
14536
|
+
if (branch.timeoutMs != null)
|
|
14537
|
+
fields.push(["timeoutMs", branch.timeoutMs]);
|
|
14538
|
+
if (branch.reason != null)
|
|
14539
|
+
fields.push(["reason", branch.reason]);
|
|
14540
|
+
return fields;
|
|
14541
|
+
}
|
|
14542
|
+
function formatCaseScriptFileAction(action) {
|
|
14543
|
+
if (action.type === "branch") {
|
|
14544
|
+
const sides = ["then", "else"].flatMap((side) => {
|
|
14545
|
+
const actions = action[side];
|
|
14546
|
+
if (!actions?.length)
|
|
14547
|
+
return [];
|
|
14548
|
+
return `${side}: [${actions.map((inner) => `{ ${formatCaseScriptFileAction(inner)} }`).join(", ")}]`;
|
|
14557
14549
|
});
|
|
14550
|
+
const body2 = [
|
|
14551
|
+
...branchFields(action).map(([key, value]) => `${key}: ${yamlScalar(value)}`),
|
|
14552
|
+
...sides
|
|
14553
|
+
].join(", ");
|
|
14554
|
+
return `${BRANCH_KEY}: { ${body2} }`;
|
|
14558
14555
|
}
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14562
|
-
|
|
14563
|
-
const id = optionalTrimmedString(record.id);
|
|
14564
|
-
const x = optionalFiniteNumber(record.x);
|
|
14565
|
-
const y = optionalFiniteNumber(record.y);
|
|
14566
|
-
const durationMs = optionalFiniteNumber(record.durationMs);
|
|
14567
|
-
if (label)
|
|
14568
|
-
request.label = label;
|
|
14569
|
-
if (id)
|
|
14570
|
-
request.id = id;
|
|
14571
|
-
if (x != null)
|
|
14572
|
-
request.x = x;
|
|
14573
|
-
if (y != null)
|
|
14574
|
-
request.y = y;
|
|
14575
|
-
if (record.double === true)
|
|
14576
|
-
request.double = true;
|
|
14577
|
-
if (durationMs != null)
|
|
14578
|
-
request.durationMs = durationMs;
|
|
14579
|
-
} else if (type === "swipe" || type === "drag") {
|
|
14580
|
-
request = { kind: type };
|
|
14581
|
-
const x = optionalFiniteNumber(record.x);
|
|
14582
|
-
const y = optionalFiniteNumber(record.y);
|
|
14583
|
-
const x2 = optionalFiniteNumber(record.x2);
|
|
14584
|
-
const y2 = optionalFiniteNumber(record.y2);
|
|
14585
|
-
const durationMs = optionalFiniteNumber(record.durationMs);
|
|
14586
|
-
if (x != null)
|
|
14587
|
-
request.x = x;
|
|
14588
|
-
if (y != null)
|
|
14589
|
-
request.y = y;
|
|
14590
|
-
if (x2 != null)
|
|
14591
|
-
request.x2 = x2;
|
|
14592
|
-
if (y2 != null)
|
|
14593
|
-
request.y2 = y2;
|
|
14594
|
-
if (durationMs != null)
|
|
14595
|
-
request.durationMs = durationMs;
|
|
14596
|
-
} else if (type === "type" || type === "input") {
|
|
14597
|
-
request = { kind: "input" };
|
|
14598
|
-
const text = optionalTrimmedString(record.text);
|
|
14599
|
-
if (text)
|
|
14600
|
-
request.text = text;
|
|
14601
|
-
} else if (type === "alert") {
|
|
14602
|
-
request = {
|
|
14603
|
-
kind: "alert",
|
|
14604
|
-
alertAction: record.alertAction === "dismiss" ? "dismiss" : "accept"
|
|
14605
|
-
};
|
|
14606
|
-
} else if (type === "activate-app" || type === "terminate-app" || type === "restart-app") {
|
|
14607
|
-
request = { kind: type };
|
|
14608
|
-
const appId = optionalTrimmedString(record.appId);
|
|
14609
|
-
if (appId)
|
|
14610
|
-
request.appId = appId;
|
|
14611
|
-
} else if (type === "background-app") {
|
|
14612
|
-
request = { kind: "background-app" };
|
|
14613
|
-
const seconds = optionalFiniteNumber(record.seconds);
|
|
14614
|
-
if (seconds != null)
|
|
14615
|
-
request.seconds = seconds;
|
|
14616
|
-
} else if (type === "open-url") {
|
|
14617
|
-
request = { kind: "open-url" };
|
|
14618
|
-
const url = optionalTrimmedString(record.url);
|
|
14619
|
-
if (url)
|
|
14620
|
-
request.url = url;
|
|
14556
|
+
const fields = orderedFields(action);
|
|
14557
|
+
const shortField = SHORT_FORM_FIELD[action.type];
|
|
14558
|
+
if (action.type === "alert" && fields.length === 0) {
|
|
14559
|
+
return `alert: ${action.alertAction}`;
|
|
14621
14560
|
}
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
const
|
|
14630
|
-
return {
|
|
14561
|
+
const only = fields[0];
|
|
14562
|
+
if (fields.length === 1 && only && only[0] === shortField) {
|
|
14563
|
+
return `${action.type}: ${yamlScalar(only[1])}`;
|
|
14564
|
+
}
|
|
14565
|
+
if (fields.length === 0) {
|
|
14566
|
+
return `${action.type}: {}`;
|
|
14567
|
+
}
|
|
14568
|
+
const body = fields.map(([key, value]) => `${key}: ${yamlScalar(value)}`).join(", ");
|
|
14569
|
+
return `${action.type}: { ${body} }`;
|
|
14631
14570
|
}
|
|
14632
|
-
function
|
|
14633
|
-
if (
|
|
14634
|
-
return
|
|
14635
|
-
|
|
14571
|
+
function formatCaseScriptFileItemLines(action, indent) {
|
|
14572
|
+
if (action.type !== "branch")
|
|
14573
|
+
return [`${indent}- ${formatCaseScriptFileAction(action)}`];
|
|
14574
|
+
const fieldIndent = `${indent} `;
|
|
14575
|
+
const lines = [`${indent}- ${BRANCH_KEY}:`];
|
|
14576
|
+
for (const [key, value] of branchFields(action)) {
|
|
14577
|
+
lines.push(`${fieldIndent}${key}: ${yamlScalar(value)}`);
|
|
14578
|
+
}
|
|
14579
|
+
for (const side of ["then", "else"]) {
|
|
14580
|
+
const actions = action[side];
|
|
14581
|
+
if (!actions?.length)
|
|
14582
|
+
continue;
|
|
14583
|
+
lines.push(`${fieldIndent}${side}:`);
|
|
14584
|
+
for (const inner of actions) {
|
|
14585
|
+
lines.push(...formatCaseScriptFileItemLines(inner, `${fieldIndent} `));
|
|
14586
|
+
}
|
|
14587
|
+
}
|
|
14588
|
+
return lines;
|
|
14636
14589
|
}
|
|
14637
|
-
function
|
|
14638
|
-
const
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
`
|
|
14642
|
-
|
|
14643
|
-
`
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
parts.push(`retries ${phases.decideRetries}`);
|
|
14590
|
+
function formatCaseScriptFile(script, options = {}) {
|
|
14591
|
+
const lines = ["version: 1"];
|
|
14592
|
+
const name = options.name?.trim();
|
|
14593
|
+
if (name)
|
|
14594
|
+
lines.push(`name: ${yamlScalar(name)}`);
|
|
14595
|
+
if (script.sourceRunId)
|
|
14596
|
+
lines.push(`sourceRunId: ${yamlScalar(script.sourceRunId)}`);
|
|
14597
|
+
lines.push("actions:");
|
|
14598
|
+
for (const action of script.actions) {
|
|
14599
|
+
lines.push(...formatCaseScriptFileItemLines(action, " "));
|
|
14648
14600
|
}
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
|
|
14652
|
-
return parts.join(" · ");
|
|
14601
|
+
return `${lines.join(`
|
|
14602
|
+
`)}
|
|
14603
|
+
`;
|
|
14653
14604
|
}
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14660
|
-
].filter((field) => field != null);
|
|
14661
|
-
if (fields.length === 0)
|
|
14662
|
-
return null;
|
|
14663
|
-
const cached = usage.cachedInputTokens ?? 0;
|
|
14664
|
-
const totalInput = (usage.inputTokens ?? 0) + cached + (usage.cacheWriteTokens ?? 0);
|
|
14665
|
-
const share = totalInput > 0 ? ` (${Math.round(cached / totalInput * 100)}% cached)` : "";
|
|
14666
|
-
return `usage ${fields.join(", ")}${share}`;
|
|
14605
|
+
|
|
14606
|
+
// ../runner-client/src/screen-check.ts
|
|
14607
|
+
var SCREEN_CHECK_POLL_MS = 400;
|
|
14608
|
+
var ASSERT_EXIT_CODE = { met: 0, notMet: 1, couldNotCheck: 2 };
|
|
14609
|
+
function abortError() {
|
|
14610
|
+
return Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
14667
14611
|
}
|
|
14668
|
-
function
|
|
14669
|
-
|
|
14670
|
-
return {
|
|
14671
|
-
id: step.id,
|
|
14672
|
-
index: step.idx + 1,
|
|
14673
|
-
summary: actionSummary(step.action),
|
|
14674
|
-
ok: step.ok,
|
|
14675
|
-
latencyMs: step.latencyMs,
|
|
14676
|
-
phases: step.phases ?? null,
|
|
14677
|
-
detail: step.detail,
|
|
14678
|
-
reason,
|
|
14679
|
-
thoughts,
|
|
14680
|
-
command: step.command ?? formatStepCommand(step.action),
|
|
14681
|
-
screenshotBase64: screenshotsByStepId[step.id] ?? null
|
|
14682
|
-
};
|
|
14612
|
+
function describeScreenCheck(check) {
|
|
14613
|
+
return check.id != null ? `${check.assertion} id: ${check.id}` : `${check.assertion}: ${check.text}`;
|
|
14683
14614
|
}
|
|
14684
|
-
function
|
|
14685
|
-
const
|
|
14686
|
-
|
|
14687
|
-
|
|
14615
|
+
function screenCheckMissMessage(options) {
|
|
14616
|
+
const seconds = Math.round(options.timeoutMs / 1000);
|
|
14617
|
+
const [kind, target] = options.id != null ? ["id", options.id] : ["text", options.text];
|
|
14618
|
+
return options.assertion === "visible" ? `Expected visible ${kind} not found within ${seconds}s: ${target}` : `Unexpected ${kind} still visible after ${seconds}s: ${target}`;
|
|
14619
|
+
}
|
|
14620
|
+
async function checkScreen(options) {
|
|
14621
|
+
const pollMs = options.pollMs ?? SCREEN_CHECK_POLL_MS;
|
|
14622
|
+
const deadline = options.now() + options.timeoutMs;
|
|
14623
|
+
for (;; ) {
|
|
14624
|
+
if (options.isAborted?.())
|
|
14625
|
+
throw abortError();
|
|
14626
|
+
let readError = null;
|
|
14627
|
+
try {
|
|
14628
|
+
const { elements } = await options.readScreen();
|
|
14629
|
+
const found = screenHasTarget(elements, options);
|
|
14630
|
+
if (options.assertion === "visible" ? found : !found)
|
|
14631
|
+
return { met: true };
|
|
14632
|
+
} catch (error) {
|
|
14633
|
+
if (options.isDeadSession?.(error))
|
|
14634
|
+
throw error;
|
|
14635
|
+
readError = error;
|
|
14636
|
+
}
|
|
14637
|
+
if (options.now() >= deadline) {
|
|
14638
|
+
if (readError) {
|
|
14639
|
+
const seconds = Math.round(options.timeoutMs / 1000);
|
|
14640
|
+
const message = readError instanceof Error ? readError.message : String(readError);
|
|
14641
|
+
throw new Error(`Could not read the screen within ${seconds}s: ${message}`);
|
|
14642
|
+
}
|
|
14643
|
+
return { met: false };
|
|
14644
|
+
}
|
|
14645
|
+
await options.sleep(pollMs);
|
|
14688
14646
|
}
|
|
14689
|
-
const caseTitles = meta.caseTitles ?? {};
|
|
14690
|
-
const tests = run.tests.map((test) => ({
|
|
14691
|
-
id: test.id,
|
|
14692
|
-
title: caseTitles[test.caseId] ?? test.caseId,
|
|
14693
|
-
status: test.status,
|
|
14694
|
-
executionMode: test.executionMode ?? null,
|
|
14695
|
-
error: test.error,
|
|
14696
|
-
startedAt: test.startedAt,
|
|
14697
|
-
finishedAt: test.finishedAt,
|
|
14698
|
-
steps: (test.steps ?? []).map((step) => mapCatalogStep(step, screenshotsByStepId))
|
|
14699
|
-
}));
|
|
14700
|
-
const primaryTitle = tests[0]?.title ?? "Run";
|
|
14701
|
-
return {
|
|
14702
|
-
id: run.id,
|
|
14703
|
-
source: "catalog",
|
|
14704
|
-
status,
|
|
14705
|
-
title: primaryTitle,
|
|
14706
|
-
appLabel: meta.appLabel ?? null,
|
|
14707
|
-
deviceLabel: meta.deviceLabel ?? null,
|
|
14708
|
-
platform: run.platform,
|
|
14709
|
-
executionMode: run.executionMode,
|
|
14710
|
-
screenMode: run.screenMode ?? null,
|
|
14711
|
-
lane: run.lane ?? null,
|
|
14712
|
-
laneWarning: run.laneWarning ?? null,
|
|
14713
|
-
error: run.error,
|
|
14714
|
-
createdAt: run.createdAt,
|
|
14715
|
-
startedAt: run.startedAt,
|
|
14716
|
-
finishedAt: run.finishedAt,
|
|
14717
|
-
tests
|
|
14718
|
-
};
|
|
14719
14647
|
}
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14648
|
+
|
|
14649
|
+
// ../runner-client/src/case-script-replay.ts
|
|
14650
|
+
var DEFAULT_ASSERT_TIMEOUT_MS = 5000;
|
|
14651
|
+
function abortError2() {
|
|
14652
|
+
return Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
14724
14653
|
}
|
|
14725
|
-
function
|
|
14726
|
-
|
|
14654
|
+
function realSleep(ms, signal) {
|
|
14655
|
+
if (ms <= 0)
|
|
14656
|
+
return Promise.resolve();
|
|
14657
|
+
return new Promise((resolve, reject) => {
|
|
14658
|
+
if (signal?.aborted) {
|
|
14659
|
+
reject(abortError2());
|
|
14660
|
+
return;
|
|
14661
|
+
}
|
|
14662
|
+
const timer = setTimeout(() => {
|
|
14663
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14664
|
+
resolve();
|
|
14665
|
+
}, ms);
|
|
14666
|
+
const onAbort = () => {
|
|
14667
|
+
clearTimeout(timer);
|
|
14668
|
+
reject(abortError2());
|
|
14669
|
+
};
|
|
14670
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
14671
|
+
});
|
|
14727
14672
|
}
|
|
14728
|
-
function
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14673
|
+
function caseScriptActionRequest(action) {
|
|
14674
|
+
switch (action.type) {
|
|
14675
|
+
case "tap": {
|
|
14676
|
+
const request = { kind: "tap" };
|
|
14677
|
+
if (action.label)
|
|
14678
|
+
request.label = action.label;
|
|
14679
|
+
if (action.id)
|
|
14680
|
+
request.id = action.id;
|
|
14681
|
+
if (action.x != null)
|
|
14682
|
+
request.x = action.x;
|
|
14683
|
+
if (action.y != null)
|
|
14684
|
+
request.y = action.y;
|
|
14685
|
+
if (action.double)
|
|
14686
|
+
request.double = true;
|
|
14687
|
+
if (action.durationMs != null)
|
|
14688
|
+
request.durationMs = action.durationMs;
|
|
14689
|
+
return request;
|
|
14690
|
+
}
|
|
14691
|
+
case "swipe":
|
|
14692
|
+
case "drag":
|
|
14693
|
+
return {
|
|
14694
|
+
kind: action.type,
|
|
14695
|
+
x: action.x,
|
|
14696
|
+
y: action.y,
|
|
14697
|
+
x2: action.x2,
|
|
14698
|
+
y2: action.y2,
|
|
14699
|
+
...action.durationMs != null ? { durationMs: action.durationMs } : {}
|
|
14700
|
+
};
|
|
14701
|
+
case "type":
|
|
14702
|
+
return { kind: "input", text: action.text };
|
|
14703
|
+
case "alert":
|
|
14704
|
+
return { kind: "alert", alertAction: action.alertAction };
|
|
14705
|
+
case "activate-app":
|
|
14706
|
+
case "terminate-app":
|
|
14707
|
+
case "restart-app":
|
|
14708
|
+
return { kind: action.type, appId: action.appId };
|
|
14709
|
+
case "background-app":
|
|
14710
|
+
return {
|
|
14711
|
+
kind: "background-app",
|
|
14712
|
+
...action.seconds != null ? { seconds: action.seconds } : {}
|
|
14713
|
+
};
|
|
14714
|
+
case "open-url":
|
|
14715
|
+
return { kind: "open-url", url: action.url };
|
|
14716
|
+
case "key":
|
|
14717
|
+
return {
|
|
14718
|
+
kind: "key",
|
|
14719
|
+
key: action.key,
|
|
14720
|
+
...action.modifiers?.length ? { modifiers: action.modifiers } : {}
|
|
14721
|
+
};
|
|
14722
|
+
case "wait":
|
|
14723
|
+
case "assert":
|
|
14724
|
+
case "branch":
|
|
14725
|
+
return null;
|
|
14726
|
+
}
|
|
14732
14727
|
}
|
|
14733
|
-
function
|
|
14734
|
-
if (
|
|
14735
|
-
return
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
const seconds = ms / 1000;
|
|
14740
|
-
if (seconds < 60)
|
|
14741
|
-
return `${seconds.toFixed(1)}s`;
|
|
14742
|
-
const minutes = Math.floor(seconds / 60);
|
|
14743
|
-
const rem = Math.round(seconds % 60);
|
|
14744
|
-
return `${minutes}m ${rem}s`;
|
|
14728
|
+
function screenCheckTimeoutSeconds(action) {
|
|
14729
|
+
if (action.type === "branch")
|
|
14730
|
+
return (action.timeoutMs ?? BRANCH_DEFAULT_TIMEOUT_MS) / 1000;
|
|
14731
|
+
if (action.timeoutMs == null)
|
|
14732
|
+
return;
|
|
14733
|
+
return Math.max(1, Math.round(action.timeoutMs / 1000));
|
|
14745
14734
|
}
|
|
14746
|
-
function
|
|
14747
|
-
if (
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
function formatRunReportHtml(doc) {
|
|
14759
|
-
const accent = statusAccent(doc.status);
|
|
14760
|
-
const screenRows = [
|
|
14761
|
-
...doc.screenMode ? [["Screen", doc.screenMode]] : [],
|
|
14762
|
-
...doc.lane ? [["Lane", doc.lane]] : [],
|
|
14763
|
-
...doc.laneWarning ? [["Lane warning", doc.laneWarning]] : []
|
|
14764
|
-
];
|
|
14765
|
-
const metaRows = [
|
|
14766
|
-
["Status", accent.label],
|
|
14767
|
-
["Source", doc.source === "catalog" ? "Catalog run" : "Manual Inspector"],
|
|
14768
|
-
["App", doc.appLabel ?? "—"],
|
|
14769
|
-
["Device", doc.deviceLabel ?? "—"],
|
|
14770
|
-
["Platform", doc.platform ?? "—"],
|
|
14771
|
-
["Mode", doc.executionMode ?? "—"],
|
|
14772
|
-
...screenRows,
|
|
14773
|
-
["Started", formatWhen(doc.startedAt ?? doc.createdAt)],
|
|
14774
|
-
["Finished", formatWhen(doc.finishedAt)],
|
|
14775
|
-
["Duration", formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt)],
|
|
14776
|
-
["Run ID", doc.id]
|
|
14777
|
-
];
|
|
14778
|
-
const testsHtml = doc.tests.map((test, testIndex) => {
|
|
14779
|
-
const testAccent = statusAccent(test.status === "passed" || test.status === "errored" || test.status === "cancelled" ? test.status : doc.status);
|
|
14780
|
-
const stepsHtml = test.steps.map((step) => {
|
|
14781
|
-
const stepStatus = step.ok ? "Passed" : "Failed";
|
|
14782
|
-
const stepColor = step.ok ? "#166534" : "#991b1b";
|
|
14783
|
-
const reasonBlock = step.reason ? `<p class="reason">${escapeHtml(step.reason)}</p>` : "";
|
|
14784
|
-
const thoughtsBlock = step.thoughts ? `<details class="thoughts"><summary>AI thoughts</summary><p>${escapeHtml(step.thoughts)}</p></details>` : "";
|
|
14785
|
-
const detailBlock = step.detail && step.detail !== step.reason ? `<p class="detail">${escapeHtml(step.detail)}</p>` : "";
|
|
14786
|
-
const shot = step.screenshotBase64 ? `<img class="shot" alt="Step ${step.index} screenshot" src="${pngDataUri(step.screenshotBase64)}" />` : `<p class="muted">No screenshot</p>`;
|
|
14787
|
-
const latency = step.latencyMs != null ? `<span class="muted">${step.latencyMs}ms</span>` : "";
|
|
14788
|
-
const phases = step.phases ? `<p class="muted phases">Phases: ${escapeHtml(formatStepPhases(step.phases))}</p>` : "";
|
|
14789
|
-
const commandBlock = step.command ? `<pre class="command"><code>${escapeHtml(step.command)}</code></pre>` : "";
|
|
14790
|
-
return `
|
|
14791
|
-
<article class="step ${step.ok ? "ok" : "fail"}">
|
|
14792
|
-
<header>
|
|
14793
|
-
<span class="idx">${step.index}</span>
|
|
14794
|
-
<div>
|
|
14795
|
-
<h3>${escapeHtml(step.summary)}</h3>
|
|
14796
|
-
<p class="step-status" style="color:${stepColor}">${stepStatus} ${latency}</p>
|
|
14797
|
-
</div>
|
|
14798
|
-
</header>
|
|
14799
|
-
${commandBlock}
|
|
14800
|
-
${phases}
|
|
14801
|
-
${reasonBlock}
|
|
14802
|
-
${detailBlock}
|
|
14803
|
-
${thoughtsBlock}
|
|
14804
|
-
${shot}
|
|
14805
|
-
</article>`;
|
|
14806
|
-
}).join(`
|
|
14807
|
-
`);
|
|
14808
|
-
return `
|
|
14809
|
-
<section class="test">
|
|
14810
|
-
<h2>
|
|
14811
|
-
<span class="test-num">${testIndex + 1}</span>
|
|
14812
|
-
${escapeHtml(test.title)}
|
|
14813
|
-
<span class="pill" style="background:${testAccent.bg};color:${testAccent.fg}">${escapeHtml(testAccent.label)}</span>
|
|
14814
|
-
</h2>
|
|
14815
|
-
${test.error ? `<p class="error">${escapeHtml(test.error)}</p>` : ""}
|
|
14816
|
-
${test.executionMode ? `<p class="muted">Mode: ${escapeHtml(test.executionMode)}</p>` : ""}
|
|
14817
|
-
<div class="steps">${stepsHtml || `<p class="muted">No steps recorded</p>`}</div>
|
|
14818
|
-
</section>`;
|
|
14819
|
-
}).join(`
|
|
14820
|
-
`);
|
|
14821
|
-
const metaHtml = metaRows.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`).join(`
|
|
14822
|
-
`);
|
|
14823
|
-
return `<!DOCTYPE html>
|
|
14824
|
-
<html lang="en">
|
|
14825
|
-
<head>
|
|
14826
|
-
<meta charset="utf-8" />
|
|
14827
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
14828
|
-
<title>Yoqa Report — ${escapeHtml(doc.title)} (${escapeHtml(accent.label)})</title>
|
|
14829
|
-
<style>
|
|
14830
|
-
:root { color-scheme: light; }
|
|
14831
|
-
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; }
|
|
14832
|
-
.wrap { max-width: 52rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
14833
|
-
.banner { border-radius: 1rem; padding: 1.25rem 1.5rem; background: ${accent.bg}; color: ${accent.fg}; margin-bottom: 1.5rem; }
|
|
14834
|
-
.banner h1 { margin: 0 0 0.35rem; font-size: 1.5rem; }
|
|
14835
|
-
.banner p { margin: 0; opacity: 0.9; }
|
|
14836
|
-
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; }
|
|
14837
|
-
table.meta th, table.meta td { text-align: left; padding: 0.65rem 1rem; border-bottom: 1px solid #e2e8f0; font-size: 0.9rem; }
|
|
14838
|
-
table.meta th { width: 8rem; color: #64748b; font-weight: 600; }
|
|
14839
|
-
.error { background: #fee2e2; color: #991b1b; padding: 0.75rem 1rem; border-radius: 0.5rem; }
|
|
14840
|
-
.test { margin-bottom: 2.5rem; }
|
|
14841
|
-
.test h2 { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 1.15rem; }
|
|
14842
|
-
.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; }
|
|
14843
|
-
.pill { display: inline-flex; align-items: center; border-radius: 999px; padding: 0.15rem 0.65rem; font-size: 0.75rem; font-weight: 600; }
|
|
14844
|
-
.step { background: #fff; border: 1px solid #e2e8f0; border-radius: 0.75rem; padding: 1rem; margin: 0.75rem 0; }
|
|
14845
|
-
.step.fail { border-color: #fecaca; }
|
|
14846
|
-
.step header { display: flex; gap: 0.75rem; align-items: flex-start; }
|
|
14847
|
-
.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; }
|
|
14848
|
-
.step.ok .idx { background: #bbf7d0; color: #166534; }
|
|
14849
|
-
.step.fail .idx { background: #fecaca; color: #991b1b; }
|
|
14850
|
-
.step h3 { margin: 0; font-size: 1rem; }
|
|
14851
|
-
.step-status { margin: 0.15rem 0 0; font-size: 0.8rem; font-weight: 600; }
|
|
14852
|
-
.command { margin: 0.5rem 0 0; padding: 0.4rem 0.65rem; background: #f1f5f9; border-radius: 0.4rem; font-size: 0.8rem; overflow-x: auto; }
|
|
14853
|
-
.command code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
14854
|
-
.reason, .detail { margin: 0.5rem 0 0; color: #475569; font-size: 0.9rem; }
|
|
14855
|
-
.thoughts { margin-top: 0.5rem; font-size: 0.85rem; color: #64748b; }
|
|
14856
|
-
.thoughts p { margin: 0.35rem 0 0; white-space: pre-wrap; }
|
|
14857
|
-
.shot { display: block; max-width: 100%; width: min(22rem, 100%); margin-top: 0.75rem; border-radius: 0.75rem; border: 1px solid #e2e8f0; }
|
|
14858
|
-
.muted { color: #94a3b8; font-size: 0.85rem; }
|
|
14859
|
-
footer { margin-top: 2rem; color: #94a3b8; font-size: 0.8rem; }
|
|
14860
|
-
@media print {
|
|
14861
|
-
body { background: #fff; }
|
|
14862
|
-
.shot { max-height: 60vh; object-fit: contain; }
|
|
14863
|
-
}
|
|
14864
|
-
</style>
|
|
14865
|
-
</head>
|
|
14866
|
-
<body>
|
|
14867
|
-
<div class="wrap">
|
|
14868
|
-
<header class="banner">
|
|
14869
|
-
<h1>${escapeHtml(doc.title)}</h1>
|
|
14870
|
-
<p>${escapeHtml(accent.label)} · Yoqa end-to-end report</p>
|
|
14871
|
-
</header>
|
|
14872
|
-
${doc.error ? `<p class="error">${escapeHtml(doc.error)}</p>` : ""}
|
|
14873
|
-
<table class="meta">${metaHtml}</table>
|
|
14874
|
-
${testsHtml}
|
|
14875
|
-
<footer>Generated by Yoqa</footer>
|
|
14876
|
-
</div>
|
|
14877
|
-
</body>
|
|
14878
|
-
</html>
|
|
14879
|
-
`;
|
|
14880
|
-
}
|
|
14881
|
-
function escapeMd(value) {
|
|
14882
|
-
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
14883
|
-
}
|
|
14884
|
-
function formatRunReportMarkdown(doc) {
|
|
14885
|
-
const accent = statusAccent(doc.status);
|
|
14886
|
-
const lines = [
|
|
14887
|
-
`# Yoqa Report — ${doc.title}`,
|
|
14888
|
-
"",
|
|
14889
|
-
`**Status:** ${accent.label}`,
|
|
14890
|
-
"",
|
|
14891
|
-
"| Field | Value |",
|
|
14892
|
-
"| --- | --- |",
|
|
14893
|
-
`| Source | ${doc.source === "catalog" ? "Catalog run" : "Manual Inspector"} |`,
|
|
14894
|
-
`| App | ${escapeMd(doc.appLabel ?? "—")} |`,
|
|
14895
|
-
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
14896
|
-
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
14897
|
-
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
14898
|
-
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
14899
|
-
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
14900
|
-
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
14901
|
-
`| Started | ${escapeMd(formatWhen(doc.startedAt ?? doc.createdAt))} |`,
|
|
14902
|
-
`| Finished | ${escapeMd(formatWhen(doc.finishedAt))} |`,
|
|
14903
|
-
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
14904
|
-
`| Run ID | \`${doc.id}\` |`,
|
|
14905
|
-
""
|
|
14906
|
-
];
|
|
14907
|
-
if (doc.error) {
|
|
14908
|
-
lines.push(`> **Error:** ${doc.error}`, "");
|
|
14909
|
-
}
|
|
14910
|
-
doc.tests.forEach((test, testIndex) => {
|
|
14911
|
-
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
14912
|
-
lines.push(`## ${testIndex + 1}. ${test.title} (${testLabel})`, "");
|
|
14913
|
-
if (test.executionMode)
|
|
14914
|
-
lines.push(`Mode: \`${test.executionMode}\``, "");
|
|
14915
|
-
if (test.error)
|
|
14916
|
-
lines.push(`**Error:** ${test.error}`, "");
|
|
14917
|
-
if (test.steps.length === 0) {
|
|
14918
|
-
lines.push("_No steps recorded_", "");
|
|
14919
|
-
return;
|
|
14920
|
-
}
|
|
14921
|
-
for (const step of test.steps) {
|
|
14922
|
-
lines.push(`### Step ${step.index}: ${step.summary}`);
|
|
14923
|
-
lines.push("");
|
|
14924
|
-
lines.push(`- Result: **${step.ok ? "Passed" : "Failed"}**`);
|
|
14925
|
-
if (step.command)
|
|
14926
|
-
lines.push(`- Command: \`${step.command.replace(/`/g, "'")}\``);
|
|
14927
|
-
if (step.latencyMs != null)
|
|
14928
|
-
lines.push(`- Latency: ${step.latencyMs}ms`);
|
|
14929
|
-
if (step.phases)
|
|
14930
|
-
lines.push(`- Phases: ${formatStepPhases(step.phases)}`);
|
|
14931
|
-
if (step.reason)
|
|
14932
|
-
lines.push(`- Reason: ${step.reason}`);
|
|
14933
|
-
if (step.detail && step.detail !== step.reason)
|
|
14934
|
-
lines.push(`- Detail: ${step.detail}`);
|
|
14935
|
-
if (step.thoughts) {
|
|
14936
|
-
lines.push("", "<details><summary>AI thoughts</summary>", "", step.thoughts, "", "</details>");
|
|
14937
|
-
}
|
|
14938
|
-
if (step.screenshotBase64) {
|
|
14939
|
-
lines.push("", `})`);
|
|
14940
|
-
} else {
|
|
14941
|
-
lines.push("", "_No screenshot_");
|
|
14942
|
-
}
|
|
14943
|
-
lines.push("");
|
|
14944
|
-
}
|
|
14945
|
-
});
|
|
14946
|
-
lines.push("---", "", "_Generated by Yoqa_", "");
|
|
14947
|
-
return lines.join(`
|
|
14948
|
-
`);
|
|
14949
|
-
}
|
|
14950
|
-
function formatRunReportGithubSummary(doc) {
|
|
14951
|
-
const accent = statusAccent(doc.status);
|
|
14952
|
-
const passed = doc.tests.filter((test) => test.status === "passed").length;
|
|
14953
|
-
const failed = doc.tests.filter((test) => test.status === "errored").length;
|
|
14954
|
-
const cancelled = doc.tests.filter((test) => test.status === "cancelled").length;
|
|
14955
|
-
const failedSteps = doc.tests.flatMap((test) => test.steps.filter((step) => !step.ok).map((step) => ({ test, step })));
|
|
14956
|
-
const lines = [
|
|
14957
|
-
`## Yoqa Report — ${doc.title}`,
|
|
14958
|
-
"",
|
|
14959
|
-
`**${accent.label}** · ${doc.tests.length} test${doc.tests.length === 1 ? "" : "s"} · ${passed} passed · ${failed} failed · ${cancelled} cancelled`,
|
|
14960
|
-
"",
|
|
14961
|
-
"| Field | Value |",
|
|
14962
|
-
"| --- | --- |",
|
|
14963
|
-
`| App | ${escapeMd(doc.appLabel ?? "—")} |`,
|
|
14964
|
-
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
14965
|
-
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
14966
|
-
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
14967
|
-
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
14968
|
-
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
14969
|
-
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
14970
|
-
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
14971
|
-
`| Run ID | \`${doc.id}\` |`,
|
|
14972
|
-
""
|
|
14973
|
-
];
|
|
14974
|
-
if (doc.error) {
|
|
14975
|
-
lines.push(`> **Error:** ${doc.error}`, "");
|
|
14976
|
-
}
|
|
14977
|
-
if (doc.tests.length > 0) {
|
|
14978
|
-
lines.push("### Tests", "", "| # | Test | Status | Steps |", "| --- | --- | --- | --- |");
|
|
14979
|
-
doc.tests.forEach((test, testIndex) => {
|
|
14980
|
-
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
14981
|
-
lines.push(`| ${testIndex + 1} | ${escapeMd(test.title)} | ${testLabel} | ${test.steps.length} |`);
|
|
14982
|
-
});
|
|
14983
|
-
lines.push("");
|
|
14984
|
-
}
|
|
14985
|
-
if (failedSteps.length > 0) {
|
|
14986
|
-
lines.push("### Failed steps", "");
|
|
14987
|
-
for (const { test, step } of failedSteps) {
|
|
14988
|
-
lines.push(`- **${escapeMd(test.title)}** · step ${step.index}: ${escapeMd(step.summary)}`);
|
|
14989
|
-
if (step.command)
|
|
14990
|
-
lines.push(` - Command: \`${escapeMd(step.command)}\``);
|
|
14991
|
-
if (step.reason)
|
|
14992
|
-
lines.push(` - Reason: ${escapeMd(step.reason)}`);
|
|
14993
|
-
if (step.detail && step.detail !== step.reason) {
|
|
14994
|
-
lines.push(` - Detail: ${escapeMd(step.detail)}`);
|
|
14995
|
-
}
|
|
14996
|
-
}
|
|
14997
|
-
lines.push("");
|
|
14998
|
-
}
|
|
14999
|
-
lines.push("Download the `yoqa-report` artifact for the full HTML report with screenshots.", "");
|
|
15000
|
-
return lines.join(`
|
|
15001
|
-
`);
|
|
15002
|
-
}
|
|
15003
|
-
// ../../node_modules/yaml/dist/index.js
|
|
15004
|
-
var composer = require_composer();
|
|
15005
|
-
var Document = require_Document();
|
|
15006
|
-
var Schema = require_Schema();
|
|
15007
|
-
var errors2 = require_errors();
|
|
15008
|
-
var Alias = require_Alias();
|
|
15009
|
-
var identity = require_identity();
|
|
15010
|
-
var Pair = require_Pair();
|
|
15011
|
-
var Scalar = require_Scalar();
|
|
15012
|
-
var YAMLMap = require_YAMLMap();
|
|
15013
|
-
var YAMLSeq = require_YAMLSeq();
|
|
15014
|
-
var cst = require_cst();
|
|
15015
|
-
var lexer = require_lexer();
|
|
15016
|
-
var lineCounter = require_line_counter();
|
|
15017
|
-
var parser = require_parser();
|
|
15018
|
-
var publicApi = require_public_api();
|
|
15019
|
-
var visit = require_visit();
|
|
15020
|
-
var $Composer = composer.Composer;
|
|
15021
|
-
var $Document = Document.Document;
|
|
15022
|
-
var $Schema = Schema.Schema;
|
|
15023
|
-
var $YAMLError = errors2.YAMLError;
|
|
15024
|
-
var $YAMLParseError = errors2.YAMLParseError;
|
|
15025
|
-
var $YAMLWarning = errors2.YAMLWarning;
|
|
15026
|
-
var $Alias = Alias.Alias;
|
|
15027
|
-
var $isAlias = identity.isAlias;
|
|
15028
|
-
var $isCollection = identity.isCollection;
|
|
15029
|
-
var $isDocument = identity.isDocument;
|
|
15030
|
-
var $isMap = identity.isMap;
|
|
15031
|
-
var $isNode = identity.isNode;
|
|
15032
|
-
var $isPair = identity.isPair;
|
|
15033
|
-
var $isScalar = identity.isScalar;
|
|
15034
|
-
var $isSeq = identity.isSeq;
|
|
15035
|
-
var $Pair = Pair.Pair;
|
|
15036
|
-
var $Scalar = Scalar.Scalar;
|
|
15037
|
-
var $YAMLMap = YAMLMap.YAMLMap;
|
|
15038
|
-
var $YAMLSeq = YAMLSeq.YAMLSeq;
|
|
15039
|
-
var $Lexer = lexer.Lexer;
|
|
15040
|
-
var $LineCounter = lineCounter.LineCounter;
|
|
15041
|
-
var $Parser = parser.Parser;
|
|
15042
|
-
var $parse = publicApi.parse;
|
|
15043
|
-
var $parseAllDocuments = publicApi.parseAllDocuments;
|
|
15044
|
-
var $parseDocument = publicApi.parseDocument;
|
|
15045
|
-
var $stringify = publicApi.stringify;
|
|
15046
|
-
var $visit = visit.visit;
|
|
15047
|
-
var $visitAsync = visit.visitAsync;
|
|
15048
|
-
|
|
15049
|
-
// ../runner-client/src/case-script-file.ts
|
|
15050
|
-
var ACTION_TYPES = Object.keys(caseScriptActionSchemaByType);
|
|
15051
|
-
var SHORT_FORM_FIELD = {
|
|
15052
|
-
tap: "label",
|
|
15053
|
-
type: "text",
|
|
15054
|
-
wait: "ms",
|
|
15055
|
-
assert: "text",
|
|
15056
|
-
alert: "alertAction",
|
|
15057
|
-
"open-url": "url",
|
|
15058
|
-
"activate-app": "appId",
|
|
15059
|
-
"terminate-app": "appId",
|
|
15060
|
-
"restart-app": "appId",
|
|
15061
|
-
"background-app": "seconds"
|
|
15062
|
-
};
|
|
15063
|
-
var TEXT_SHORT_FORM_FIELDS = new Set(["label", "text", "alertAction", "url", "appId"]);
|
|
15064
|
-
var FIELD_ORDER = [
|
|
15065
|
-
"label",
|
|
15066
|
-
"id",
|
|
15067
|
-
"text",
|
|
15068
|
-
"appId",
|
|
15069
|
-
"url",
|
|
15070
|
-
"assertion",
|
|
15071
|
-
"alertAction",
|
|
15072
|
-
"x",
|
|
15073
|
-
"y",
|
|
15074
|
-
"x2",
|
|
15075
|
-
"y2",
|
|
15076
|
-
"ms",
|
|
15077
|
-
"seconds",
|
|
15078
|
-
"timeoutMs",
|
|
15079
|
-
"double",
|
|
15080
|
-
"durationMs",
|
|
15081
|
-
"reason"
|
|
15082
|
-
];
|
|
15083
|
-
var TOP_LEVEL_KEYS = new Set(["version", "name", "sourceRunId", "savedAt", "actions"]);
|
|
15084
|
-
function isActionType(value) {
|
|
15085
|
-
return typeof value === "string" && ACTION_TYPES.includes(value);
|
|
14735
|
+
function caseScriptActionCommand(action) {
|
|
14736
|
+
if (action.type === "wait") {
|
|
14737
|
+
const seconds = action.ms / 1000;
|
|
14738
|
+
return `sleep ${Number.isInteger(seconds) ? seconds : Number(seconds.toFixed(3))}`;
|
|
14739
|
+
}
|
|
14740
|
+
if (action.type === "assert" || action.type === "branch") {
|
|
14741
|
+
const timeoutSeconds = screenCheckTimeoutSeconds(action);
|
|
14742
|
+
const timeout = timeoutSeconds != null ? ` --timeout ${timeoutSeconds}` : "";
|
|
14743
|
+
return ["yoqa", "assert", action.assertion, ...assertTargetFlags(action)].join(" ") + timeout;
|
|
14744
|
+
}
|
|
14745
|
+
const request = caseScriptActionRequest(action);
|
|
14746
|
+
return request ? formatActionShellLine(request) : action.type;
|
|
15086
14747
|
}
|
|
15087
|
-
function
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
14748
|
+
function formatCaseScriptStepPath(path) {
|
|
14749
|
+
return [
|
|
14750
|
+
`action ${path.index + 1}`,
|
|
14751
|
+
...path.nested.map((step) => `${step.side} → action ${step.index + 1}`)
|
|
14752
|
+
].join(" → ");
|
|
15091
14753
|
}
|
|
15092
|
-
function
|
|
15093
|
-
const
|
|
15094
|
-
|
|
14754
|
+
function formatCaseScriptReplayStep(event) {
|
|
14755
|
+
const { action } = event;
|
|
14756
|
+
const line = action.type === "branch" ? `if ${describeScreenCheck(action)}${event.outcome ? ` → ${event.outcome}` : ""}` : formatCaseScriptFileAction(action);
|
|
14757
|
+
return `${" ".repeat(event.path.nested.length)}${line}`;
|
|
15095
14758
|
}
|
|
15096
|
-
function
|
|
15097
|
-
|
|
14759
|
+
function replayErrorMessage(error, signal) {
|
|
14760
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError")
|
|
14761
|
+
return "Aborted";
|
|
14762
|
+
if (error instanceof Error)
|
|
14763
|
+
return error.message;
|
|
14764
|
+
return String(error);
|
|
15098
14765
|
}
|
|
15099
|
-
function
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
let
|
|
15105
|
-
let
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
}
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
14766
|
+
async function runCaseScript(client, script, options = {}) {
|
|
14767
|
+
const sleep = options.sleep ?? realSleep;
|
|
14768
|
+
const now = options.now ?? Date.now;
|
|
14769
|
+
const pollMs = options.assertPollMs ?? SCREEN_CHECK_POLL_MS;
|
|
14770
|
+
let total = script.actions.length;
|
|
14771
|
+
let completed = 0;
|
|
14772
|
+
let position = 0;
|
|
14773
|
+
const check = async (action, timeoutMs) => checkScreen({
|
|
14774
|
+
readScreen: () => client.getScreen(),
|
|
14775
|
+
text: action.text,
|
|
14776
|
+
id: action.id,
|
|
14777
|
+
assertion: action.assertion,
|
|
14778
|
+
timeoutMs,
|
|
14779
|
+
pollMs,
|
|
14780
|
+
sleep: (ms) => sleep(ms, options.signal),
|
|
14781
|
+
now,
|
|
14782
|
+
isAborted: () => options.signal?.aborted ?? false
|
|
14783
|
+
});
|
|
14784
|
+
const assertText = async (action) => {
|
|
14785
|
+
const timeoutMs = action.timeoutMs ?? options.defaultAssertTimeoutMs ?? DEFAULT_ASSERT_TIMEOUT_MS;
|
|
14786
|
+
const { met } = await check(action, timeoutMs);
|
|
14787
|
+
if (!met)
|
|
14788
|
+
throw new Error(screenCheckMissMessage({ ...action, timeoutMs }));
|
|
14789
|
+
};
|
|
14790
|
+
const emit = (path, stepPosition, action, status, extra = {}) => options.onStep?.({
|
|
14791
|
+
index: path.index,
|
|
14792
|
+
path,
|
|
14793
|
+
position: stepPosition,
|
|
14794
|
+
total,
|
|
14795
|
+
action,
|
|
14796
|
+
status,
|
|
14797
|
+
...extra
|
|
14798
|
+
});
|
|
14799
|
+
const skipFrom = async (actions, from, at) => {
|
|
14800
|
+
for (let index = from;index < actions.length; index++) {
|
|
14801
|
+
const action = actions[index];
|
|
14802
|
+
if (action)
|
|
14803
|
+
await emit(at(index), position++, action, "skipped");
|
|
15126
14804
|
}
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
|
|
15134
|
-
|
|
15135
|
-
|
|
15136
|
-
|
|
15137
|
-
|
|
14805
|
+
};
|
|
14806
|
+
const replay = async (actions, at) => {
|
|
14807
|
+
for (let index = 0;index < actions.length; index++) {
|
|
14808
|
+
const action = actions[index];
|
|
14809
|
+
if (!action)
|
|
14810
|
+
continue;
|
|
14811
|
+
const path = at(index);
|
|
14812
|
+
if (options.signal?.aborted) {
|
|
14813
|
+
await skipFrom(actions, index, at);
|
|
14814
|
+
return { path, error: "Aborted" };
|
|
14815
|
+
}
|
|
14816
|
+
const step = position++;
|
|
14817
|
+
await emit(path, step, action, "running");
|
|
14818
|
+
let side = null;
|
|
14819
|
+
try {
|
|
14820
|
+
let response;
|
|
14821
|
+
if (action.type === "wait") {
|
|
14822
|
+
await sleep(action.ms, options.signal);
|
|
14823
|
+
} else if (action.type === "assert") {
|
|
14824
|
+
await assertText(action);
|
|
14825
|
+
} else if (action.type === "branch") {
|
|
14826
|
+
const timeoutMs = action.timeoutMs ?? options.defaultBranchTimeoutMs ?? BRANCH_DEFAULT_TIMEOUT_MS;
|
|
14827
|
+
const { met } = await check(action, timeoutMs);
|
|
14828
|
+
const outcome = met ? "then" : "else";
|
|
14829
|
+
side = { outcome, actions: (met ? action.then : action.else) ?? [] };
|
|
14830
|
+
total += side.actions.length;
|
|
14831
|
+
} else {
|
|
14832
|
+
const request = caseScriptActionRequest(action);
|
|
14833
|
+
if (request)
|
|
14834
|
+
response = await client.performAction(request);
|
|
14835
|
+
if (options.pauseAfterActionMs)
|
|
14836
|
+
await sleep(options.pauseAfterActionMs, options.signal);
|
|
14837
|
+
}
|
|
14838
|
+
completed += 1;
|
|
14839
|
+
await emit(path, step, action, "ok", {
|
|
14840
|
+
...response ? { response } : {},
|
|
14841
|
+
...side ? { outcome: side.outcome } : {}
|
|
14842
|
+
});
|
|
14843
|
+
} catch (error) {
|
|
14844
|
+
const message = replayErrorMessage(error, options.signal);
|
|
14845
|
+
await emit(path, step, action, "error", { error: message });
|
|
14846
|
+
await skipFrom(actions, index + 1, at);
|
|
14847
|
+
return { path, error: message };
|
|
14848
|
+
}
|
|
14849
|
+
if (side) {
|
|
14850
|
+
const outcome = side.outcome;
|
|
14851
|
+
const failure2 = await replay(side.actions, (inner) => ({
|
|
14852
|
+
index: path.index,
|
|
14853
|
+
nested: [...path.nested, { side: outcome, index: inner }]
|
|
14854
|
+
}));
|
|
14855
|
+
if (failure2) {
|
|
14856
|
+
await skipFrom(actions, index + 1, at);
|
|
14857
|
+
return failure2;
|
|
14858
|
+
}
|
|
15138
14859
|
}
|
|
15139
|
-
fields = {
|
|
15140
|
-
[field]: TEXT_SHORT_FORM_FIELDS.has(field) && typeof value !== "string" ? String(value) : value
|
|
15141
|
-
};
|
|
15142
14860
|
}
|
|
15143
|
-
|
|
15144
|
-
|
|
15145
|
-
const
|
|
15146
|
-
if (
|
|
14861
|
+
return null;
|
|
14862
|
+
};
|
|
14863
|
+
const failure = await replay(script.actions, (index) => ({ index, nested: [] }));
|
|
14864
|
+
if (failure) {
|
|
15147
14865
|
return {
|
|
15148
14866
|
ok: false,
|
|
15149
|
-
|
|
14867
|
+
completed,
|
|
14868
|
+
total,
|
|
14869
|
+
failedIndex: failure.path.index,
|
|
14870
|
+
failedPath: failure.path,
|
|
14871
|
+
error: failure.error
|
|
15150
14872
|
};
|
|
15151
14873
|
}
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
14874
|
+
return { ok: true, completed, total };
|
|
14875
|
+
}
|
|
14876
|
+
|
|
14877
|
+
// ../runner-client/src/script-format.ts
|
|
14878
|
+
function shellSingleQuote2(value) {
|
|
14879
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14880
|
+
}
|
|
14881
|
+
function actionToShellLine(action) {
|
|
14882
|
+
if (action.type === "tap") {
|
|
14883
|
+
const parts = ["yoqa", "action", "tap"];
|
|
14884
|
+
if (action.label) {
|
|
14885
|
+
parts.push("--label", shellSingleQuote2(action.label));
|
|
14886
|
+
} else if (action.id) {
|
|
14887
|
+
parts.push("--id", shellSingleQuote2(action.id));
|
|
14888
|
+
} else {
|
|
14889
|
+
parts.push("--x", String(Math.round(action.x ?? 0)), "--y", String(Math.round(action.y ?? 0)));
|
|
14890
|
+
}
|
|
14891
|
+
if (action.double)
|
|
14892
|
+
parts.push("--double");
|
|
14893
|
+
if (action.durationMs != null)
|
|
14894
|
+
parts.push("--duration", String(Math.round(action.durationMs)));
|
|
14895
|
+
return parts.join(" ");
|
|
15155
14896
|
}
|
|
15156
|
-
|
|
14897
|
+
if (action.type === "swipe") {
|
|
14898
|
+
const parts = [
|
|
14899
|
+
"yoqa",
|
|
14900
|
+
"action",
|
|
14901
|
+
"swipe",
|
|
14902
|
+
"--x",
|
|
14903
|
+
String(Math.round(action.x)),
|
|
14904
|
+
"--y",
|
|
14905
|
+
String(Math.round(action.y)),
|
|
14906
|
+
"--x2",
|
|
14907
|
+
String(Math.round(action.x2)),
|
|
14908
|
+
"--y2",
|
|
14909
|
+
String(Math.round(action.y2))
|
|
14910
|
+
];
|
|
14911
|
+
if (action.durationMs != null) {
|
|
14912
|
+
parts.push("--duration", String(Math.round(action.durationMs)));
|
|
14913
|
+
}
|
|
14914
|
+
return parts.join(" ");
|
|
14915
|
+
}
|
|
14916
|
+
if (action.type === "drag") {
|
|
14917
|
+
const parts = [
|
|
14918
|
+
"yoqa",
|
|
14919
|
+
"action",
|
|
14920
|
+
"drag",
|
|
14921
|
+
"--x",
|
|
14922
|
+
String(Math.round(action.x)),
|
|
14923
|
+
"--y",
|
|
14924
|
+
String(Math.round(action.y)),
|
|
14925
|
+
"--x2",
|
|
14926
|
+
String(Math.round(action.x2)),
|
|
14927
|
+
"--y2",
|
|
14928
|
+
String(Math.round(action.y2))
|
|
14929
|
+
];
|
|
14930
|
+
if (action.durationMs != null) {
|
|
14931
|
+
parts.push("--duration", String(Math.round(action.durationMs)));
|
|
14932
|
+
}
|
|
14933
|
+
return parts.join(" ");
|
|
14934
|
+
}
|
|
14935
|
+
if (action.type === "type") {
|
|
14936
|
+
return `yoqa action input --text ${shellSingleQuote2(action.text)}`;
|
|
14937
|
+
}
|
|
14938
|
+
if (action.type === "assert") {
|
|
14939
|
+
const timeoutSeconds = action.timeoutMs != null ? Math.max(1, Math.round(action.timeoutMs / 1000)) : undefined;
|
|
14940
|
+
const parts = ["yoqa", "assert", action.assertion, "--text", shellSingleQuote2(action.text)];
|
|
14941
|
+
if (timeoutSeconds != null) {
|
|
14942
|
+
parts.push("--timeout", String(timeoutSeconds));
|
|
14943
|
+
}
|
|
14944
|
+
return parts.join(" ");
|
|
14945
|
+
}
|
|
14946
|
+
if (action.type === "alert") {
|
|
14947
|
+
return action.alertAction === "dismiss" ? "yoqa action alert --dismiss" : "yoqa action alert";
|
|
14948
|
+
}
|
|
14949
|
+
if (action.type === "activate-app" || action.type === "terminate-app" || action.type === "restart-app") {
|
|
14950
|
+
return `yoqa action ${action.type} --app-id ${shellSingleQuote2(action.appId)}`;
|
|
14951
|
+
}
|
|
14952
|
+
if (action.type === "background-app") {
|
|
14953
|
+
return action.seconds != null ? `yoqa action background-app --seconds ${action.seconds}` : "yoqa action background-app";
|
|
14954
|
+
}
|
|
14955
|
+
if (action.type === "open-url") {
|
|
14956
|
+
return `yoqa action open-url --url ${shellSingleQuote2(action.url)}`;
|
|
14957
|
+
}
|
|
14958
|
+
if (action.type === "key") {
|
|
14959
|
+
return action.modifiers?.length ? `yoqa action key ${action.key} --modifiers ${action.modifiers.join(",")}` : `yoqa action key ${action.key}`;
|
|
14960
|
+
}
|
|
14961
|
+
if (action.type === "branch") {
|
|
14962
|
+
const check = caseScriptActionCommand(action);
|
|
14963
|
+
const side = (actions) => actions.map((inner) => indentLines(actionToShellLine(inner)));
|
|
14964
|
+
return [
|
|
14965
|
+
`if ${BRANCH_HELPER} ${check}; then`,
|
|
14966
|
+
...side(action.then),
|
|
14967
|
+
...action.else?.length ? ["else", ...side(action.else)] : [],
|
|
14968
|
+
"fi"
|
|
14969
|
+
].join(`
|
|
14970
|
+
`);
|
|
14971
|
+
}
|
|
14972
|
+
const seconds = Math.max(0.1, action.ms / 1000);
|
|
14973
|
+
return `sleep ${seconds}`;
|
|
15157
14974
|
}
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
|
|
15163
|
-
|
|
14975
|
+
var BRANCH_HELPER = "yoqa_branch";
|
|
14976
|
+
var BRANCH_HELPER_LINES = [
|
|
14977
|
+
"# A Branch's check: 0 runs `then`, 1 runs `else`, anything else stops the script.",
|
|
14978
|
+
`${BRANCH_HELPER}() {`,
|
|
14979
|
+
" local status=0",
|
|
14980
|
+
' "$@" || status=$?',
|
|
14981
|
+
' [ "$status" -le 1 ] || exit "$status"',
|
|
14982
|
+
' return "$status"',
|
|
14983
|
+
"}",
|
|
14984
|
+
""
|
|
14985
|
+
];
|
|
14986
|
+
function indentLines(text) {
|
|
14987
|
+
return text.split(`
|
|
14988
|
+
`).map((line) => ` ${line}`).join(`
|
|
14989
|
+
`);
|
|
15164
14990
|
}
|
|
15165
|
-
function
|
|
15166
|
-
const
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
14991
|
+
function formatCaseScriptShell(script, meta = {}) {
|
|
14992
|
+
const titleParts = [
|
|
14993
|
+
meta.caseNumber != null ? `#${meta.caseNumber}` : null,
|
|
14994
|
+
meta.caseName?.trim() || null
|
|
14995
|
+
].filter(Boolean);
|
|
14996
|
+
const title = titleParts.length > 0 ? titleParts.join(" ") : "exported script";
|
|
14997
|
+
const lines = [
|
|
14998
|
+
"#!/usr/bin/env bash",
|
|
14999
|
+
`# Yoqa exported script — ${title}`,
|
|
15000
|
+
"# Requires an active device session: yoqa devices connect <id>",
|
|
15001
|
+
"# Prefer the Case Script file: `yoqa script run <file.yoqa.yaml>`.",
|
|
15002
|
+
"set -euo pipefail",
|
|
15003
|
+
"",
|
|
15004
|
+
...script.actions.some((action) => action.type === "branch") ? BRANCH_HELPER_LINES : [],
|
|
15005
|
+
...script.actions.map(actionToShellLine),
|
|
15006
|
+
""
|
|
15007
|
+
];
|
|
15008
|
+
return lines.join(`
|
|
15009
|
+
`);
|
|
15010
|
+
}
|
|
15011
|
+
// ../runner-client/src/viewport.ts
|
|
15012
|
+
var VIEWPORT_PRESETS = {
|
|
15013
|
+
desktop: { name: "desktop", width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
|
|
15014
|
+
mobile: { name: "mobile", width: 390, height: 844, deviceScaleFactor: 3, mobile: true }
|
|
15015
|
+
};
|
|
15016
|
+
function formatViewportPreset(name) {
|
|
15017
|
+
const { width, height } = VIEWPORT_PRESETS[name];
|
|
15018
|
+
return `${name} (${width}×${height})`;
|
|
15019
|
+
}
|
|
15020
|
+
|
|
15021
|
+
// ../runner-client/src/run-report.ts
|
|
15022
|
+
function readStepCycle(action) {
|
|
15023
|
+
if (!action || typeof action !== "object" || !("cycle" in action))
|
|
15024
|
+
return null;
|
|
15025
|
+
const cycle = action.cycle;
|
|
15026
|
+
if (!cycle || typeof cycle !== "object")
|
|
15027
|
+
return null;
|
|
15028
|
+
const record = cycle;
|
|
15029
|
+
const asRecord = (value) => value && typeof value === "object" ? value : null;
|
|
15030
|
+
const verify = asRecord(record.verify);
|
|
15031
|
+
if (!verify || typeof verify.type !== "string" || !verify.type)
|
|
15032
|
+
return null;
|
|
15033
|
+
return {
|
|
15034
|
+
action: asRecord(record.action),
|
|
15035
|
+
decision: asRecord(record.decision),
|
|
15036
|
+
verify: {
|
|
15037
|
+
type: verify.type,
|
|
15038
|
+
reason: typeof verify.reason === "string" ? verify.reason : null,
|
|
15039
|
+
thoughts: typeof verify.thoughts === "string" ? verify.thoughts : null
|
|
15040
|
+
}
|
|
15041
|
+
};
|
|
15042
|
+
}
|
|
15043
|
+
function summarizeActionRecord(record) {
|
|
15044
|
+
const type = typeof record.type === "string" ? record.type : "step";
|
|
15045
|
+
if (type === "tap") {
|
|
15046
|
+
if (typeof record.label === "string" && record.label.trim())
|
|
15047
|
+
return `Tap: ${record.label}`;
|
|
15048
|
+
if (typeof record.id === "string" && record.id.trim())
|
|
15049
|
+
return `Tap #${record.id}`;
|
|
15050
|
+
return "Tap";
|
|
15170
15051
|
}
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
};
|
|
15052
|
+
if (type === "type")
|
|
15053
|
+
return `Type${typeof record.text === "string" ? `: ${record.text}` : ""}`;
|
|
15054
|
+
if (type === "wait")
|
|
15055
|
+
return "Wait";
|
|
15056
|
+
if (type === "alert") {
|
|
15057
|
+
return record.alertAction === "dismiss" ? "Dismiss alert" : "Accept alert";
|
|
15058
|
+
}
|
|
15059
|
+
if (type === "assert") {
|
|
15060
|
+
const assertion = typeof record.assertion === "string" ? record.assertion : "visible";
|
|
15061
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
15062
|
+
return text ? `Assert ${assertion}: ${text}` : `Assert ${assertion}`;
|
|
15183
15063
|
}
|
|
15184
|
-
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
const
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
line: lineOf(pair.key, lineCounter2),
|
|
15191
|
-
action: null,
|
|
15192
|
-
message: `unknown key "${String(key)}" (allowed: ${[...TOP_LEVEL_KEYS].join(", ")})`
|
|
15193
|
-
});
|
|
15194
|
-
}
|
|
15064
|
+
if (type === "branch") {
|
|
15065
|
+
const assertion = record.assertion === "not-visible" ? "not-visible" : "visible";
|
|
15066
|
+
const id = typeof record.id === "string" ? record.id : undefined;
|
|
15067
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
15068
|
+
const outcome = typeof record.outcome === "string" ? ` → ${record.outcome}` : "";
|
|
15069
|
+
return `If ${describeScreenCheck({ assertion, text, id })}${outcome}`;
|
|
15195
15070
|
}
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15199
|
-
|
|
15200
|
-
|
|
15201
|
-
|
|
15202
|
-
|
|
15071
|
+
if (type === "verify")
|
|
15072
|
+
return "Verify";
|
|
15073
|
+
if (type === "done")
|
|
15074
|
+
return "Done";
|
|
15075
|
+
if (type === "fail")
|
|
15076
|
+
return "Failed";
|
|
15077
|
+
if (type === "swipe") {
|
|
15078
|
+
if (typeof record.direction === "string" && record.direction.trim()) {
|
|
15079
|
+
return `Swipe ${record.direction}`;
|
|
15080
|
+
}
|
|
15081
|
+
const x = typeof record.x === "number" ? record.x : null;
|
|
15082
|
+
const y = typeof record.y === "number" ? record.y : null;
|
|
15083
|
+
const x2 = typeof record.x2 === "number" ? record.x2 : null;
|
|
15084
|
+
const y2 = typeof record.y2 === "number" ? record.y2 : null;
|
|
15085
|
+
if (y != null && y2 != null && x != null && x2 != null) {
|
|
15086
|
+
const dy = y2 - y;
|
|
15087
|
+
const dx = x2 - x;
|
|
15088
|
+
if (Math.abs(dy) >= Math.abs(dx)) {
|
|
15089
|
+
if (dy < -50)
|
|
15090
|
+
return "Swipe up";
|
|
15091
|
+
if (dy > 50)
|
|
15092
|
+
return "Swipe down";
|
|
15093
|
+
} else {
|
|
15094
|
+
if (dx < -50)
|
|
15095
|
+
return "Swipe left";
|
|
15096
|
+
if (dx > 50)
|
|
15097
|
+
return "Swipe right";
|
|
15098
|
+
}
|
|
15099
|
+
}
|
|
15100
|
+
return "Swipe";
|
|
15203
15101
|
}
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
errors3.push({
|
|
15207
|
-
line: lineOf(root.get("name", true), lineCounter2),
|
|
15208
|
-
action: null,
|
|
15209
|
-
message: "name must be text"
|
|
15210
|
-
});
|
|
15102
|
+
if (type === "input") {
|
|
15103
|
+
return `Input${typeof record.text === "string" ? `: ${record.text}` : ""}`;
|
|
15211
15104
|
}
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15105
|
+
if (type === "drag")
|
|
15106
|
+
return "Drag";
|
|
15107
|
+
if (type === "activate-app")
|
|
15108
|
+
return "Activate app";
|
|
15109
|
+
if (type === "terminate-app")
|
|
15110
|
+
return "Terminate app";
|
|
15111
|
+
if (type === "restart-app")
|
|
15112
|
+
return "Restart app";
|
|
15113
|
+
if (type === "background-app")
|
|
15114
|
+
return "Background app";
|
|
15115
|
+
if (type === "key") {
|
|
15116
|
+
const modifiers = Array.isArray(record.modifiers) ? record.modifiers.filter((value) => typeof value === "string") : [];
|
|
15117
|
+
return typeof record.key === "string" ? `Press ${formatKeyChord(record.key, modifiers)}` : "Press key";
|
|
15219
15118
|
}
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
if (legacy && (typeof savedAt !== "number" || !Number.isInteger(savedAt) || savedAt < 0)) {
|
|
15223
|
-
errors3.push({
|
|
15224
|
-
line: lineOf(root.get("savedAt", true), lineCounter2),
|
|
15225
|
-
action: null,
|
|
15226
|
-
message: "savedAt must be a non-negative whole number"
|
|
15227
|
-
});
|
|
15119
|
+
if (type === "open-url") {
|
|
15120
|
+
return typeof record.url === "string" && record.url.trim() ? `Open URL: ${record.url}` : "Open URL";
|
|
15228
15121
|
}
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
if (
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15122
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
15123
|
+
}
|
|
15124
|
+
function actionSummary(action) {
|
|
15125
|
+
if (!action || typeof action !== "object")
|
|
15126
|
+
return "Step";
|
|
15127
|
+
const cycle = readStepCycle(action);
|
|
15128
|
+
if (cycle?.action)
|
|
15129
|
+
return summarizeActionRecord(cycle.action);
|
|
15130
|
+
if (cycle?.decision)
|
|
15131
|
+
return summarizeActionRecord(cycle.decision);
|
|
15132
|
+
return summarizeActionRecord(action);
|
|
15133
|
+
}
|
|
15134
|
+
function optionalTrimmedString(value) {
|
|
15135
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
15136
|
+
}
|
|
15137
|
+
function optionalFiniteNumber(value) {
|
|
15138
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
15139
|
+
}
|
|
15140
|
+
function formatStepCommand(action) {
|
|
15141
|
+
if (!action || typeof action !== "object")
|
|
15142
|
+
return null;
|
|
15143
|
+
const record = action;
|
|
15144
|
+
const type = typeof record.type === "string" ? record.type : null;
|
|
15145
|
+
if (type == null || type === "verify" || type === "done" || type === "fail")
|
|
15146
|
+
return null;
|
|
15147
|
+
if (type === "wait") {
|
|
15148
|
+
const ms = optionalFiniteNumber(record.ms) ?? 0;
|
|
15149
|
+
return formatSleepShellLine(ms / 1000);
|
|
15254
15150
|
}
|
|
15255
|
-
if (
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15151
|
+
if (type === "assert" || type === "branch") {
|
|
15152
|
+
const text = optionalTrimmedString(record.text);
|
|
15153
|
+
const id = type === "branch" ? optionalTrimmedString(record.id) : undefined;
|
|
15154
|
+
if (!text && !id)
|
|
15155
|
+
return null;
|
|
15156
|
+
const assertion = record.assertion === "not-visible" ? "not-visible" : "visible";
|
|
15157
|
+
const timeoutMs = optionalFiniteNumber(record.timeoutMs);
|
|
15158
|
+
return formatAssertShellLine({
|
|
15159
|
+
assertion,
|
|
15160
|
+
...id ? { id } : { text },
|
|
15161
|
+
...timeoutMs != null ? { timeoutSeconds: timeoutMs / 1000 } : {}
|
|
15260
15162
|
});
|
|
15261
15163
|
}
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15164
|
+
let request = null;
|
|
15165
|
+
if (type === "tap") {
|
|
15166
|
+
request = { kind: "tap" };
|
|
15167
|
+
const label = optionalTrimmedString(record.label);
|
|
15168
|
+
const id = optionalTrimmedString(record.id);
|
|
15169
|
+
const x = optionalFiniteNumber(record.x);
|
|
15170
|
+
const y = optionalFiniteNumber(record.y);
|
|
15171
|
+
const durationMs = optionalFiniteNumber(record.durationMs);
|
|
15172
|
+
if (label)
|
|
15173
|
+
request.label = label;
|
|
15174
|
+
if (id)
|
|
15175
|
+
request.id = id;
|
|
15176
|
+
if (x != null)
|
|
15177
|
+
request.x = x;
|
|
15178
|
+
if (y != null)
|
|
15179
|
+
request.y = y;
|
|
15180
|
+
if (record.double === true)
|
|
15181
|
+
request.double = true;
|
|
15182
|
+
if (durationMs != null)
|
|
15183
|
+
request.durationMs = durationMs;
|
|
15184
|
+
} else if (type === "swipe" || type === "drag") {
|
|
15185
|
+
request = { kind: type };
|
|
15186
|
+
const x = optionalFiniteNumber(record.x);
|
|
15187
|
+
const y = optionalFiniteNumber(record.y);
|
|
15188
|
+
const x2 = optionalFiniteNumber(record.x2);
|
|
15189
|
+
const y2 = optionalFiniteNumber(record.y2);
|
|
15190
|
+
const durationMs = optionalFiniteNumber(record.durationMs);
|
|
15191
|
+
if (x != null)
|
|
15192
|
+
request.x = x;
|
|
15193
|
+
if (y != null)
|
|
15194
|
+
request.y = y;
|
|
15195
|
+
if (x2 != null)
|
|
15196
|
+
request.x2 = x2;
|
|
15197
|
+
if (y2 != null)
|
|
15198
|
+
request.y2 = y2;
|
|
15199
|
+
if (durationMs != null)
|
|
15200
|
+
request.durationMs = durationMs;
|
|
15201
|
+
} else if (type === "type" || type === "input") {
|
|
15202
|
+
request = { kind: "input" };
|
|
15203
|
+
const text = optionalTrimmedString(record.text);
|
|
15204
|
+
if (text)
|
|
15205
|
+
request.text = text;
|
|
15206
|
+
} else if (type === "alert") {
|
|
15207
|
+
request = {
|
|
15208
|
+
kind: "alert",
|
|
15209
|
+
alertAction: record.alertAction === "dismiss" ? "dismiss" : "accept"
|
|
15210
|
+
};
|
|
15211
|
+
} else if (type === "activate-app" || type === "terminate-app" || type === "restart-app") {
|
|
15212
|
+
request = { kind: type };
|
|
15213
|
+
const appId = optionalTrimmedString(record.appId);
|
|
15214
|
+
if (appId)
|
|
15215
|
+
request.appId = appId;
|
|
15216
|
+
} else if (type === "background-app") {
|
|
15217
|
+
request = { kind: "background-app" };
|
|
15218
|
+
const seconds = optionalFiniteNumber(record.seconds);
|
|
15219
|
+
if (seconds != null)
|
|
15220
|
+
request.seconds = seconds;
|
|
15221
|
+
} else if (type === "key") {
|
|
15222
|
+
const key = keyNameSchema.safeParse(record.key);
|
|
15223
|
+
const modifiers = exports_external.array(keyModifierSchema).safeParse(record.modifiers);
|
|
15224
|
+
if (!key.success)
|
|
15225
|
+
return null;
|
|
15226
|
+
request = {
|
|
15227
|
+
kind: "key",
|
|
15228
|
+
key: key.data,
|
|
15229
|
+
...modifiers.success && modifiers.data.length > 0 ? { modifiers: modifiers.data } : {}
|
|
15266
15230
|
};
|
|
15231
|
+
} else if (type === "open-url") {
|
|
15232
|
+
request = { kind: "open-url" };
|
|
15233
|
+
const url = optionalTrimmedString(record.url);
|
|
15234
|
+
if (url)
|
|
15235
|
+
request.url = url;
|
|
15236
|
+
}
|
|
15237
|
+
return request ? formatActionShellLine(request) : null;
|
|
15238
|
+
}
|
|
15239
|
+
function stepReasoning(step) {
|
|
15240
|
+
const action = step.action && typeof step.action === "object" ? step.action : null;
|
|
15241
|
+
const reasonFromAction = typeof action?.reason === "string" && action.reason.trim() ? action.reason.trim() : null;
|
|
15242
|
+
const thoughtsFromAction = typeof action?.thoughts === "string" && action.thoughts.trim() ? action.thoughts.trim() : null;
|
|
15243
|
+
const reason = reasonFromAction ?? (step.detail?.trim() ? step.detail.trim() : null);
|
|
15244
|
+
const thoughts = thoughtsFromAction && thoughtsFromAction !== reason ? thoughtsFromAction : null;
|
|
15245
|
+
return { reason, thoughts };
|
|
15246
|
+
}
|
|
15247
|
+
function toReportStatus(status) {
|
|
15248
|
+
if (status === "passed" || status === "errored" || status === "cancelled")
|
|
15249
|
+
return status;
|
|
15250
|
+
return null;
|
|
15251
|
+
}
|
|
15252
|
+
function formatStepPhases(phases) {
|
|
15253
|
+
const parts = [
|
|
15254
|
+
`capture ${Math.round(phases.captureMs)}ms`,
|
|
15255
|
+
`screen ${Math.round(phases.screenMs)}ms`,
|
|
15256
|
+
`image ${Math.round(phases.prepareMs)}ms`,
|
|
15257
|
+
`decide ${Math.round(phases.decideMs)}ms`,
|
|
15258
|
+
`action ${Math.round(phases.actionMs)}ms`,
|
|
15259
|
+
`settle ${Math.round(phases.settleMs)}ms`
|
|
15260
|
+
];
|
|
15261
|
+
if (phases.decideRetries > 0) {
|
|
15262
|
+
parts.push(`retries ${phases.decideRetries}`);
|
|
15263
|
+
}
|
|
15264
|
+
const usage = phases.usage ? formatCallUsage(phases.usage) : null;
|
|
15265
|
+
if (usage)
|
|
15266
|
+
parts.push(usage);
|
|
15267
|
+
return parts.join(" · ");
|
|
15268
|
+
}
|
|
15269
|
+
function formatCallUsage(usage) {
|
|
15270
|
+
const fields = [
|
|
15271
|
+
usage.inputTokens != null ? `in ${usage.inputTokens}` : null,
|
|
15272
|
+
usage.cachedInputTokens != null ? `cached ${usage.cachedInputTokens}` : null,
|
|
15273
|
+
usage.cacheWriteTokens != null ? `cache write ${usage.cacheWriteTokens}` : null,
|
|
15274
|
+
usage.outputTokens != null ? `out ${usage.outputTokens}` : null
|
|
15275
|
+
].filter((field) => field != null);
|
|
15276
|
+
if (fields.length === 0)
|
|
15277
|
+
return null;
|
|
15278
|
+
const cached = usage.cachedInputTokens ?? 0;
|
|
15279
|
+
const totalInput = (usage.inputTokens ?? 0) + cached + (usage.cacheWriteTokens ?? 0);
|
|
15280
|
+
const share = totalInput > 0 ? ` (${Math.round(cached / totalInput * 100)}% cached)` : "";
|
|
15281
|
+
return `usage ${fields.join(", ")}${share}`;
|
|
15282
|
+
}
|
|
15283
|
+
function mapCatalogStep(step, screenshotsByStepId) {
|
|
15284
|
+
const { reason, thoughts } = stepReasoning(step);
|
|
15285
|
+
return {
|
|
15286
|
+
id: step.id,
|
|
15287
|
+
index: step.idx + 1,
|
|
15288
|
+
summary: actionSummary(step.action),
|
|
15289
|
+
ok: step.ok,
|
|
15290
|
+
latencyMs: step.latencyMs,
|
|
15291
|
+
phases: step.phases ?? null,
|
|
15292
|
+
detail: step.detail,
|
|
15293
|
+
reason,
|
|
15294
|
+
thoughts,
|
|
15295
|
+
command: step.command ?? formatStepCommand(step.action),
|
|
15296
|
+
screenshotBase64: screenshotsByStepId[step.id] ?? null
|
|
15297
|
+
};
|
|
15298
|
+
}
|
|
15299
|
+
function buildRunReportFromCatalogRun(run, meta = {}, screenshotsByStepId = {}) {
|
|
15300
|
+
const status = toReportStatus(run.status);
|
|
15301
|
+
if (!status) {
|
|
15302
|
+
throw new Error(`Cannot export report for live run status: ${run.status}`);
|
|
15267
15303
|
}
|
|
15304
|
+
const caseTitles = meta.caseTitles ?? {};
|
|
15305
|
+
const tests = run.tests.map((test) => ({
|
|
15306
|
+
id: test.id,
|
|
15307
|
+
title: caseTitles[test.caseId] ?? test.caseId,
|
|
15308
|
+
status: test.status,
|
|
15309
|
+
executionMode: test.executionMode ?? null,
|
|
15310
|
+
error: test.error,
|
|
15311
|
+
startedAt: test.startedAt,
|
|
15312
|
+
finishedAt: test.finishedAt,
|
|
15313
|
+
steps: (test.steps ?? []).map((step) => mapCatalogStep(step, screenshotsByStepId))
|
|
15314
|
+
}));
|
|
15315
|
+
const primaryTitle = tests[0]?.title ?? "Run";
|
|
15268
15316
|
return {
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15317
|
+
id: run.id,
|
|
15318
|
+
source: "catalog",
|
|
15319
|
+
status,
|
|
15320
|
+
title: primaryTitle,
|
|
15321
|
+
appLabel: meta.appLabel ?? null,
|
|
15322
|
+
deviceLabel: meta.deviceLabel ?? null,
|
|
15323
|
+
platform: run.platform,
|
|
15324
|
+
executionMode: run.executionMode,
|
|
15325
|
+
screenMode: run.screenMode ?? null,
|
|
15326
|
+
lane: run.lane ?? null,
|
|
15327
|
+
laneWarning: run.laneWarning ?? null,
|
|
15328
|
+
browser: run.browser ?? null,
|
|
15329
|
+
viewport: run.viewport ?? null,
|
|
15330
|
+
error: run.error,
|
|
15331
|
+
createdAt: run.createdAt,
|
|
15332
|
+
startedAt: run.startedAt,
|
|
15333
|
+
finishedAt: run.finishedAt,
|
|
15334
|
+
tests
|
|
15278
15335
|
};
|
|
15279
15336
|
}
|
|
15280
|
-
function
|
|
15281
|
-
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
error.action != null ? `action ${error.action}` : null
|
|
15285
|
-
].filter(Boolean).join(", ");
|
|
15286
|
-
return where ? `${where}: ${error.message}` : error.message;
|
|
15287
|
-
}).join(`
|
|
15288
|
-
`);
|
|
15337
|
+
function suggestedRunReportBasename(doc) {
|
|
15338
|
+
const shortId = doc.id.replace(/[^a-zA-Z0-9]+/g, "").slice(0, 8) || "run";
|
|
15339
|
+
const raw = ["yoqa-run", shortId, doc.status].join("-").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
15340
|
+
return raw || "yoqa-run-report";
|
|
15289
15341
|
}
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
lineWidth: 0,
|
|
15293
|
-
blockQuote: false,
|
|
15294
|
-
flowCollectionPadding: false
|
|
15295
|
-
};
|
|
15296
|
-
function yamlScalar(value) {
|
|
15297
|
-
if (typeof value === "string" && /[\n\r\t]/.test(value))
|
|
15298
|
-
return JSON.stringify(value);
|
|
15299
|
-
return $stringify([value], FLOW_OPTIONS).trim().slice(1, -1);
|
|
15342
|
+
function escapeHtml(value) {
|
|
15343
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15300
15344
|
}
|
|
15301
|
-
function
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
const at = FIELD_ORDER.indexOf(key);
|
|
15306
|
-
return at < 0 ? FIELD_ORDER.length : at;
|
|
15307
|
-
};
|
|
15308
|
-
return meaningful.sort(([a], [b]) => rank(a) - rank(b));
|
|
15345
|
+
function formatWhen(ms) {
|
|
15346
|
+
if (ms == null)
|
|
15347
|
+
return "—";
|
|
15348
|
+
return new Date(ms).toLocaleString();
|
|
15309
15349
|
}
|
|
15310
|
-
function
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
const
|
|
15317
|
-
if (
|
|
15318
|
-
return `${
|
|
15350
|
+
function formatDuration(startedAt, finishedAt) {
|
|
15351
|
+
if (startedAt == null || finishedAt == null)
|
|
15352
|
+
return "—";
|
|
15353
|
+
const ms = Math.max(0, finishedAt - startedAt);
|
|
15354
|
+
if (ms < 1000)
|
|
15355
|
+
return `${ms}ms`;
|
|
15356
|
+
const seconds = ms / 1000;
|
|
15357
|
+
if (seconds < 60)
|
|
15358
|
+
return `${seconds.toFixed(1)}s`;
|
|
15359
|
+
const minutes = Math.floor(seconds / 60);
|
|
15360
|
+
const rem = Math.round(seconds % 60);
|
|
15361
|
+
return `${minutes}m ${rem}s`;
|
|
15362
|
+
}
|
|
15363
|
+
function statusAccent(status) {
|
|
15364
|
+
if (status === "passed") {
|
|
15365
|
+
return { bg: "#dcfce7", fg: "#166534", label: "Passed" };
|
|
15319
15366
|
}
|
|
15320
|
-
if (
|
|
15321
|
-
return
|
|
15367
|
+
if (status === "errored") {
|
|
15368
|
+
return { bg: "#fee2e2", fg: "#991b1b", label: "Failed" };
|
|
15322
15369
|
}
|
|
15323
|
-
|
|
15324
|
-
return `${action.type}: { ${body} }`;
|
|
15370
|
+
return { bg: "#e5e7eb", fg: "#374151", label: "Cancelled" };
|
|
15325
15371
|
}
|
|
15326
|
-
function
|
|
15327
|
-
return `
|
|
15372
|
+
function pngDataUri(base64) {
|
|
15373
|
+
return `data:image/png;base64,${base64}`;
|
|
15328
15374
|
}
|
|
15329
|
-
function
|
|
15330
|
-
const
|
|
15331
|
-
const
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
|
|
15335
|
-
|
|
15336
|
-
|
|
15337
|
-
|
|
15338
|
-
|
|
15375
|
+
function formatRunReportHtml(doc) {
|
|
15376
|
+
const accent = statusAccent(doc.status);
|
|
15377
|
+
const screenRows = [
|
|
15378
|
+
...doc.screenMode ? [["Screen", doc.screenMode]] : [],
|
|
15379
|
+
...doc.lane ? [["Lane", doc.lane]] : [],
|
|
15380
|
+
...doc.laneWarning ? [["Lane warning", doc.laneWarning]] : []
|
|
15381
|
+
];
|
|
15382
|
+
const metaRows = [
|
|
15383
|
+
["Status", accent.label],
|
|
15384
|
+
["Source", doc.source === "catalog" ? "Catalog run" : "Manual Inspector"],
|
|
15385
|
+
["App", doc.appLabel ?? "—"],
|
|
15386
|
+
["Device", doc.deviceLabel ?? "—"],
|
|
15387
|
+
["Platform", doc.platform ?? "—"],
|
|
15388
|
+
...doc.browser ? [["Browser", doc.browser]] : [],
|
|
15389
|
+
...doc.viewport ? [["Viewport", formatViewportPreset(doc.viewport)]] : [],
|
|
15390
|
+
["Mode", doc.executionMode ?? "—"],
|
|
15391
|
+
...screenRows,
|
|
15392
|
+
["Started", formatWhen(doc.startedAt ?? doc.createdAt)],
|
|
15393
|
+
["Finished", formatWhen(doc.finishedAt)],
|
|
15394
|
+
["Duration", formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt)],
|
|
15395
|
+
["Run ID", doc.id]
|
|
15396
|
+
];
|
|
15397
|
+
const testsHtml = doc.tests.map((test, testIndex) => {
|
|
15398
|
+
const testAccent = statusAccent(test.status === "passed" || test.status === "errored" || test.status === "cancelled" ? test.status : doc.status);
|
|
15399
|
+
const stepsHtml = test.steps.map((step) => {
|
|
15400
|
+
const stepStatus = step.ok ? "Passed" : "Failed";
|
|
15401
|
+
const stepColor = step.ok ? "#166534" : "#991b1b";
|
|
15402
|
+
const reasonBlock = step.reason ? `<p class="reason">${escapeHtml(step.reason)}</p>` : "";
|
|
15403
|
+
const thoughtsBlock = step.thoughts ? `<details class="thoughts"><summary>AI thoughts</summary><p>${escapeHtml(step.thoughts)}</p></details>` : "";
|
|
15404
|
+
const detailBlock = step.detail && step.detail !== step.reason ? `<p class="detail">${escapeHtml(step.detail)}</p>` : "";
|
|
15405
|
+
const shot = step.screenshotBase64 ? `<img class="shot" alt="Step ${step.index} screenshot" src="${pngDataUri(step.screenshotBase64)}" />` : `<p class="muted">No screenshot</p>`;
|
|
15406
|
+
const latency = step.latencyMs != null ? `<span class="muted">${step.latencyMs}ms</span>` : "";
|
|
15407
|
+
const phases = step.phases ? `<p class="muted phases">Phases: ${escapeHtml(formatStepPhases(step.phases))}</p>` : "";
|
|
15408
|
+
const commandBlock = step.command ? `<pre class="command"><code>${escapeHtml(step.command)}</code></pre>` : "";
|
|
15409
|
+
return `
|
|
15410
|
+
<article class="step ${step.ok ? "ok" : "fail"}">
|
|
15411
|
+
<header>
|
|
15412
|
+
<span class="idx">${step.index}</span>
|
|
15413
|
+
<div>
|
|
15414
|
+
<h3>${escapeHtml(step.summary)}</h3>
|
|
15415
|
+
<p class="step-status" style="color:${stepColor}">${stepStatus} ${latency}</p>
|
|
15416
|
+
</div>
|
|
15417
|
+
</header>
|
|
15418
|
+
${commandBlock}
|
|
15419
|
+
${phases}
|
|
15420
|
+
${reasonBlock}
|
|
15421
|
+
${detailBlock}
|
|
15422
|
+
${thoughtsBlock}
|
|
15423
|
+
${shot}
|
|
15424
|
+
</article>`;
|
|
15425
|
+
}).join(`
|
|
15426
|
+
`);
|
|
15427
|
+
return `
|
|
15428
|
+
<section class="test">
|
|
15429
|
+
<h2>
|
|
15430
|
+
<span class="test-num">${testIndex + 1}</span>
|
|
15431
|
+
${escapeHtml(test.title)}
|
|
15432
|
+
<span class="pill" style="background:${testAccent.bg};color:${testAccent.fg}">${escapeHtml(testAccent.label)}</span>
|
|
15433
|
+
</h2>
|
|
15434
|
+
${test.error ? `<p class="error">${escapeHtml(test.error)}</p>` : ""}
|
|
15435
|
+
${test.executionMode ? `<p class="muted">Mode: ${escapeHtml(test.executionMode)}</p>` : ""}
|
|
15436
|
+
<div class="steps">${stepsHtml || `<p class="muted">No steps recorded</p>`}</div>
|
|
15437
|
+
</section>`;
|
|
15438
|
+
}).join(`
|
|
15439
|
+
`);
|
|
15440
|
+
const metaHtml = metaRows.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`).join(`
|
|
15441
|
+
`);
|
|
15442
|
+
return `<!DOCTYPE html>
|
|
15443
|
+
<html lang="en">
|
|
15444
|
+
<head>
|
|
15445
|
+
<meta charset="utf-8" />
|
|
15446
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
15447
|
+
<title>Yoqa Report — ${escapeHtml(doc.title)} (${escapeHtml(accent.label)})</title>
|
|
15448
|
+
<style>
|
|
15449
|
+
:root { color-scheme: light; }
|
|
15450
|
+
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; }
|
|
15451
|
+
.wrap { max-width: 52rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
15452
|
+
.banner { border-radius: 1rem; padding: 1.25rem 1.5rem; background: ${accent.bg}; color: ${accent.fg}; margin-bottom: 1.5rem; }
|
|
15453
|
+
.banner h1 { margin: 0 0 0.35rem; font-size: 1.5rem; }
|
|
15454
|
+
.banner p { margin: 0; opacity: 0.9; }
|
|
15455
|
+
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; }
|
|
15456
|
+
table.meta th, table.meta td { text-align: left; padding: 0.65rem 1rem; border-bottom: 1px solid #e2e8f0; font-size: 0.9rem; }
|
|
15457
|
+
table.meta th { width: 8rem; color: #64748b; font-weight: 600; }
|
|
15458
|
+
.error { background: #fee2e2; color: #991b1b; padding: 0.75rem 1rem; border-radius: 0.5rem; }
|
|
15459
|
+
.test { margin-bottom: 2.5rem; }
|
|
15460
|
+
.test h2 { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 1.15rem; }
|
|
15461
|
+
.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; }
|
|
15462
|
+
.pill { display: inline-flex; align-items: center; border-radius: 999px; padding: 0.15rem 0.65rem; font-size: 0.75rem; font-weight: 600; }
|
|
15463
|
+
.step { background: #fff; border: 1px solid #e2e8f0; border-radius: 0.75rem; padding: 1rem; margin: 0.75rem 0; }
|
|
15464
|
+
.step.fail { border-color: #fecaca; }
|
|
15465
|
+
.step header { display: flex; gap: 0.75rem; align-items: flex-start; }
|
|
15466
|
+
.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; }
|
|
15467
|
+
.step.ok .idx { background: #bbf7d0; color: #166534; }
|
|
15468
|
+
.step.fail .idx { background: #fecaca; color: #991b1b; }
|
|
15469
|
+
.step h3 { margin: 0; font-size: 1rem; }
|
|
15470
|
+
.step-status { margin: 0.15rem 0 0; font-size: 0.8rem; font-weight: 600; }
|
|
15471
|
+
.command { margin: 0.5rem 0 0; padding: 0.4rem 0.65rem; background: #f1f5f9; border-radius: 0.4rem; font-size: 0.8rem; overflow-x: auto; }
|
|
15472
|
+
.command code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
15473
|
+
.reason, .detail { margin: 0.5rem 0 0; color: #475569; font-size: 0.9rem; }
|
|
15474
|
+
.thoughts { margin-top: 0.5rem; font-size: 0.85rem; color: #64748b; }
|
|
15475
|
+
.thoughts p { margin: 0.35rem 0 0; white-space: pre-wrap; }
|
|
15476
|
+
.shot { display: block; max-width: 100%; width: min(22rem, 100%); margin-top: 0.75rem; border-radius: 0.75rem; border: 1px solid #e2e8f0; }
|
|
15477
|
+
.muted { color: #94a3b8; font-size: 0.85rem; }
|
|
15478
|
+
footer { margin-top: 2rem; color: #94a3b8; font-size: 0.8rem; }
|
|
15479
|
+
@media print {
|
|
15480
|
+
body { background: #fff; }
|
|
15481
|
+
.shot { max-height: 60vh; object-fit: contain; }
|
|
15339
15482
|
}
|
|
15340
|
-
|
|
15341
|
-
|
|
15483
|
+
</style>
|
|
15484
|
+
</head>
|
|
15485
|
+
<body>
|
|
15486
|
+
<div class="wrap">
|
|
15487
|
+
<header class="banner">
|
|
15488
|
+
<h1>${escapeHtml(doc.title)}</h1>
|
|
15489
|
+
<p>${escapeHtml(accent.label)} · Yoqa end-to-end report</p>
|
|
15490
|
+
</header>
|
|
15491
|
+
${doc.error ? `<p class="error">${escapeHtml(doc.error)}</p>` : ""}
|
|
15492
|
+
<table class="meta">${metaHtml}</table>
|
|
15493
|
+
${testsHtml}
|
|
15494
|
+
<footer>Generated by Yoqa</footer>
|
|
15495
|
+
</div>
|
|
15496
|
+
</body>
|
|
15497
|
+
</html>
|
|
15342
15498
|
`;
|
|
15343
15499
|
}
|
|
15344
|
-
|
|
15345
|
-
|
|
15346
|
-
function abortError() {
|
|
15347
|
-
return Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
15500
|
+
function escapeMd(value) {
|
|
15501
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
15348
15502
|
}
|
|
15349
|
-
function
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15503
|
+
function formatRunReportMarkdown(doc) {
|
|
15504
|
+
const accent = statusAccent(doc.status);
|
|
15505
|
+
const lines = [
|
|
15506
|
+
`# Yoqa Report — ${doc.title}`,
|
|
15507
|
+
"",
|
|
15508
|
+
`**Status:** ${accent.label}`,
|
|
15509
|
+
"",
|
|
15510
|
+
"| Field | Value |",
|
|
15511
|
+
"| --- | --- |",
|
|
15512
|
+
`| Source | ${doc.source === "catalog" ? "Catalog run" : "Manual Inspector"} |`,
|
|
15513
|
+
`| App | ${escapeMd(doc.appLabel ?? "—")} |`,
|
|
15514
|
+
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
15515
|
+
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
15516
|
+
...doc.browser ? [`| Browser | ${escapeMd(doc.browser)} |`] : [],
|
|
15517
|
+
...doc.viewport ? [`| Viewport | ${escapeMd(formatViewportPreset(doc.viewport))} |`] : [],
|
|
15518
|
+
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
15519
|
+
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
15520
|
+
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
15521
|
+
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
15522
|
+
`| Started | ${escapeMd(formatWhen(doc.startedAt ?? doc.createdAt))} |`,
|
|
15523
|
+
`| Finished | ${escapeMd(formatWhen(doc.finishedAt))} |`,
|
|
15524
|
+
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
15525
|
+
`| Run ID | \`${doc.id}\` |`,
|
|
15526
|
+
""
|
|
15527
|
+
];
|
|
15528
|
+
if (doc.error) {
|
|
15529
|
+
lines.push(`> **Error:** ${doc.error}`, "");
|
|
15530
|
+
}
|
|
15531
|
+
doc.tests.forEach((test, testIndex) => {
|
|
15532
|
+
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
15533
|
+
lines.push(`## ${testIndex + 1}. ${test.title} (${testLabel})`, "");
|
|
15534
|
+
if (test.executionMode)
|
|
15535
|
+
lines.push(`Mode: \`${test.executionMode}\``, "");
|
|
15536
|
+
if (test.error)
|
|
15537
|
+
lines.push(`**Error:** ${test.error}`, "");
|
|
15538
|
+
if (test.steps.length === 0) {
|
|
15539
|
+
lines.push("_No steps recorded_", "");
|
|
15355
15540
|
return;
|
|
15356
15541
|
}
|
|
15357
|
-
const
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15372
|
-
|
|
15373
|
-
|
|
15374
|
-
if (
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
|
|
15380
|
-
if (action.double)
|
|
15381
|
-
request.double = true;
|
|
15382
|
-
if (action.durationMs != null)
|
|
15383
|
-
request.durationMs = action.durationMs;
|
|
15384
|
-
return request;
|
|
15542
|
+
for (const step of test.steps) {
|
|
15543
|
+
lines.push(`### Step ${step.index}: ${step.summary}`);
|
|
15544
|
+
lines.push("");
|
|
15545
|
+
lines.push(`- Result: **${step.ok ? "Passed" : "Failed"}**`);
|
|
15546
|
+
if (step.command)
|
|
15547
|
+
lines.push(`- Command: \`${step.command.replace(/`/g, "'")}\``);
|
|
15548
|
+
if (step.latencyMs != null)
|
|
15549
|
+
lines.push(`- Latency: ${step.latencyMs}ms`);
|
|
15550
|
+
if (step.phases)
|
|
15551
|
+
lines.push(`- Phases: ${formatStepPhases(step.phases)}`);
|
|
15552
|
+
if (step.reason)
|
|
15553
|
+
lines.push(`- Reason: ${step.reason}`);
|
|
15554
|
+
if (step.detail && step.detail !== step.reason)
|
|
15555
|
+
lines.push(`- Detail: ${step.detail}`);
|
|
15556
|
+
if (step.thoughts) {
|
|
15557
|
+
lines.push("", "<details><summary>AI thoughts</summary>", "", step.thoughts, "", "</details>");
|
|
15558
|
+
}
|
|
15559
|
+
if (step.screenshotBase64) {
|
|
15560
|
+
lines.push("", `})`);
|
|
15561
|
+
} else {
|
|
15562
|
+
lines.push("", "_No screenshot_");
|
|
15563
|
+
}
|
|
15564
|
+
lines.push("");
|
|
15385
15565
|
}
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
x: action.x,
|
|
15391
|
-
y: action.y,
|
|
15392
|
-
x2: action.x2,
|
|
15393
|
-
y2: action.y2,
|
|
15394
|
-
...action.durationMs != null ? { durationMs: action.durationMs } : {}
|
|
15395
|
-
};
|
|
15396
|
-
case "type":
|
|
15397
|
-
return { kind: "input", text: action.text };
|
|
15398
|
-
case "alert":
|
|
15399
|
-
return { kind: "alert", alertAction: action.alertAction };
|
|
15400
|
-
case "activate-app":
|
|
15401
|
-
case "terminate-app":
|
|
15402
|
-
case "restart-app":
|
|
15403
|
-
return { kind: action.type, appId: action.appId };
|
|
15404
|
-
case "background-app":
|
|
15405
|
-
return {
|
|
15406
|
-
kind: "background-app",
|
|
15407
|
-
...action.seconds != null ? { seconds: action.seconds } : {}
|
|
15408
|
-
};
|
|
15409
|
-
case "open-url":
|
|
15410
|
-
return { kind: "open-url", url: action.url };
|
|
15411
|
-
case "wait":
|
|
15412
|
-
case "assert":
|
|
15413
|
-
return null;
|
|
15414
|
-
}
|
|
15566
|
+
});
|
|
15567
|
+
lines.push("---", "", "_Generated by Yoqa_", "");
|
|
15568
|
+
return lines.join(`
|
|
15569
|
+
`);
|
|
15415
15570
|
}
|
|
15416
|
-
function
|
|
15417
|
-
|
|
15418
|
-
|
|
15419
|
-
|
|
15571
|
+
function formatRunReportGithubSummary(doc) {
|
|
15572
|
+
const accent = statusAccent(doc.status);
|
|
15573
|
+
const passed = doc.tests.filter((test) => test.status === "passed").length;
|
|
15574
|
+
const failed = doc.tests.filter((test) => test.status === "errored").length;
|
|
15575
|
+
const cancelled = doc.tests.filter((test) => test.status === "cancelled").length;
|
|
15576
|
+
const failedSteps = doc.tests.flatMap((test) => test.steps.filter((step) => !step.ok).map((step) => ({ test, step })));
|
|
15577
|
+
const lines = [
|
|
15578
|
+
`## Yoqa Report — ${doc.title}`,
|
|
15579
|
+
"",
|
|
15580
|
+
`**${accent.label}** · ${doc.tests.length} test${doc.tests.length === 1 ? "" : "s"} · ${passed} passed · ${failed} failed · ${cancelled} cancelled`,
|
|
15581
|
+
"",
|
|
15582
|
+
"| Field | Value |",
|
|
15583
|
+
"| --- | --- |",
|
|
15584
|
+
`| App | ${escapeMd(doc.appLabel ?? "—")} |`,
|
|
15585
|
+
`| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
|
|
15586
|
+
`| Platform | ${escapeMd(doc.platform ?? "—")} |`,
|
|
15587
|
+
...doc.browser ? [`| Browser | ${escapeMd(doc.browser)} |`] : [],
|
|
15588
|
+
...doc.viewport ? [`| Viewport | ${escapeMd(formatViewportPreset(doc.viewport))} |`] : [],
|
|
15589
|
+
`| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
|
|
15590
|
+
...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
|
|
15591
|
+
...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
|
|
15592
|
+
...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
|
|
15593
|
+
`| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
|
|
15594
|
+
`| Run ID | \`${doc.id}\` |`,
|
|
15595
|
+
""
|
|
15596
|
+
];
|
|
15597
|
+
if (doc.error) {
|
|
15598
|
+
lines.push(`> **Error:** ${doc.error}`, "");
|
|
15420
15599
|
}
|
|
15421
|
-
if (
|
|
15422
|
-
|
|
15423
|
-
|
|
15600
|
+
if (doc.tests.length > 0) {
|
|
15601
|
+
lines.push("### Tests", "", "| # | Test | Status | Steps |", "| --- | --- | --- | --- |");
|
|
15602
|
+
doc.tests.forEach((test, testIndex) => {
|
|
15603
|
+
const testLabel = test.status === "passed" ? "Passed" : test.status === "errored" ? "Failed" : test.status === "cancelled" ? "Cancelled" : test.status;
|
|
15604
|
+
lines.push(`| ${testIndex + 1} | ${escapeMd(test.title)} | ${testLabel} | ${test.steps.length} |`);
|
|
15605
|
+
});
|
|
15606
|
+
lines.push("");
|
|
15424
15607
|
}
|
|
15425
|
-
|
|
15426
|
-
|
|
15427
|
-
}
|
|
15428
|
-
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
for (let index = from;index < total; index++) {
|
|
15436
|
-
const action = actions[index];
|
|
15437
|
-
if (action)
|
|
15438
|
-
await options.onStep?.({ index, total, action, status: "skipped" });
|
|
15439
|
-
}
|
|
15440
|
-
};
|
|
15441
|
-
const assertText = async (action) => {
|
|
15442
|
-
const timeoutMs = action.timeoutMs ?? options.defaultAssertTimeoutMs ?? DEFAULT_ASSERT_TIMEOUT_MS;
|
|
15443
|
-
const deadline = now() + timeoutMs;
|
|
15444
|
-
const seconds = Math.round(timeoutMs / 1000);
|
|
15445
|
-
for (;; ) {
|
|
15446
|
-
if (options.signal?.aborted)
|
|
15447
|
-
throw abortError();
|
|
15448
|
-
let readError = null;
|
|
15449
|
-
try {
|
|
15450
|
-
const screen = await client.getScreen();
|
|
15451
|
-
const found = screenHasText(screen.elements, action.text);
|
|
15452
|
-
if (action.assertion === "visible" ? found : !found)
|
|
15453
|
-
return;
|
|
15454
|
-
} catch (error) {
|
|
15455
|
-
readError = error;
|
|
15456
|
-
}
|
|
15457
|
-
if (now() >= deadline) {
|
|
15458
|
-
if (readError) {
|
|
15459
|
-
const message = readError instanceof Error ? readError.message : String(readError);
|
|
15460
|
-
throw new Error(`Could not read the screen within ${seconds}s: ${message}`);
|
|
15461
|
-
}
|
|
15462
|
-
throw new Error(action.assertion === "visible" ? `Expected visible text not found within ${seconds}s: ${action.text}` : `Unexpected text still visible after ${seconds}s: ${action.text}`);
|
|
15608
|
+
if (failedSteps.length > 0) {
|
|
15609
|
+
lines.push("### Failed steps", "");
|
|
15610
|
+
for (const { test, step } of failedSteps) {
|
|
15611
|
+
lines.push(`- **${escapeMd(test.title)}** · step ${step.index}: ${escapeMd(step.summary)}`);
|
|
15612
|
+
if (step.command)
|
|
15613
|
+
lines.push(` - Command: \`${escapeMd(step.command)}\``);
|
|
15614
|
+
if (step.reason)
|
|
15615
|
+
lines.push(` - Reason: ${escapeMd(step.reason)}`);
|
|
15616
|
+
if (step.detail && step.detail !== step.reason) {
|
|
15617
|
+
lines.push(` - Detail: ${escapeMd(step.detail)}`);
|
|
15463
15618
|
}
|
|
15464
|
-
await sleep(pollMs, options.signal);
|
|
15465
|
-
}
|
|
15466
|
-
};
|
|
15467
|
-
for (let index = 0;index < total; index++) {
|
|
15468
|
-
const action = actions[index];
|
|
15469
|
-
if (!action)
|
|
15470
|
-
continue;
|
|
15471
|
-
if (options.signal?.aborted) {
|
|
15472
|
-
await skipFrom(index);
|
|
15473
|
-
return { ok: false, completed: index, total, failedIndex: index, error: "Aborted" };
|
|
15474
|
-
}
|
|
15475
|
-
await options.onStep?.({ index, total, action, status: "running" });
|
|
15476
|
-
try {
|
|
15477
|
-
let response;
|
|
15478
|
-
if (action.type === "wait") {
|
|
15479
|
-
await sleep(action.ms, options.signal);
|
|
15480
|
-
} else if (action.type === "assert") {
|
|
15481
|
-
await assertText(action);
|
|
15482
|
-
} else {
|
|
15483
|
-
const request = caseScriptActionRequest(action);
|
|
15484
|
-
if (request)
|
|
15485
|
-
response = await client.performAction(request);
|
|
15486
|
-
if (options.pauseAfterActionMs)
|
|
15487
|
-
await sleep(options.pauseAfterActionMs, options.signal);
|
|
15488
|
-
}
|
|
15489
|
-
await options.onStep?.({
|
|
15490
|
-
index,
|
|
15491
|
-
total,
|
|
15492
|
-
action,
|
|
15493
|
-
status: "ok",
|
|
15494
|
-
...response ? { response } : {}
|
|
15495
|
-
});
|
|
15496
|
-
} catch (error) {
|
|
15497
|
-
const aborted = options.signal?.aborted || error instanceof Error && error.name === "AbortError";
|
|
15498
|
-
const message = aborted ? "Aborted" : error instanceof Error ? error.message : String(error);
|
|
15499
|
-
await options.onStep?.({ index, total, action, status: "error", error: message });
|
|
15500
|
-
await skipFrom(index + 1);
|
|
15501
|
-
return { ok: false, completed: index, total, failedIndex: index, error: message };
|
|
15502
15619
|
}
|
|
15620
|
+
lines.push("");
|
|
15503
15621
|
}
|
|
15504
|
-
|
|
15622
|
+
lines.push("Download the `yoqa-report` artifact for the full HTML report with screenshots.", "");
|
|
15623
|
+
return lines.join(`
|
|
15624
|
+
`);
|
|
15505
15625
|
}
|
|
15506
15626
|
// ../runner-client/src/index.ts
|
|
15507
15627
|
var DEFAULT_BASE_URL = "http://127.0.0.1:7420";
|
|
@@ -16392,7 +16512,7 @@ function recommendScreenDefault(vision, tree) {
|
|
|
16392
16512
|
// package.json
|
|
16393
16513
|
var package_default = {
|
|
16394
16514
|
name: "@yoqa/cli",
|
|
16395
|
-
version: "1.0.
|
|
16515
|
+
version: "1.0.3",
|
|
16396
16516
|
description: "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
|
|
16397
16517
|
license: "UNLICENSED",
|
|
16398
16518
|
type: "module",
|
|
@@ -16429,7 +16549,7 @@ var package_default = {
|
|
|
16429
16549
|
health: "bun run src/main.ts health"
|
|
16430
16550
|
},
|
|
16431
16551
|
dependencies: {
|
|
16432
|
-
"@yoqa/runner": "1.0.
|
|
16552
|
+
"@yoqa/runner": "1.0.3"
|
|
16433
16553
|
},
|
|
16434
16554
|
devDependencies: {
|
|
16435
16555
|
"@types/node": "^22.15.30",
|
|
@@ -16919,6 +17039,41 @@ function commandChainIncludes(command, name) {
|
|
|
16919
17039
|
return false;
|
|
16920
17040
|
}
|
|
16921
17041
|
|
|
17042
|
+
// src/platform.ts
|
|
17043
|
+
var PLATFORM_DEVICES_DESCRIPTION = {
|
|
17044
|
+
ios: "List iOS devices and simulators",
|
|
17045
|
+
android: "List Android devices and emulators",
|
|
17046
|
+
web: "List installed web browsers (attach a Chromium you started by naming its port: chromium-cdp-<port>)"
|
|
17047
|
+
};
|
|
17048
|
+
function parsePlatform(value) {
|
|
17049
|
+
const parsed = devicePlatformSchema.safeParse(value);
|
|
17050
|
+
if (!parsed.success) {
|
|
17051
|
+
const names = devicePlatformSchema.options.map((option) => option.value);
|
|
17052
|
+
throw new Error(`--platform must be ${names.slice(0, -1).join(", ")} or ${names.at(-1)}`);
|
|
17053
|
+
}
|
|
17054
|
+
return parsed.data;
|
|
17055
|
+
}
|
|
17056
|
+
var WEB_DEVICE_ID = /^(web-(chrome|edge|brave|chromium)|chromium-cdp-\d+)$/;
|
|
17057
|
+
function isWebDeviceId(deviceId) {
|
|
17058
|
+
return WEB_DEVICE_ID.test(deviceId);
|
|
17059
|
+
}
|
|
17060
|
+
function connectPlatform(deviceId, value) {
|
|
17061
|
+
if (value === undefined && isWebDeviceId(deviceId))
|
|
17062
|
+
return "web";
|
|
17063
|
+
return parsePlatform(value);
|
|
17064
|
+
}
|
|
17065
|
+
function parseViewportPreset(value, platform) {
|
|
17066
|
+
if (value === undefined)
|
|
17067
|
+
return;
|
|
17068
|
+
if (platform !== "web") {
|
|
17069
|
+
throw new Error("--viewport is for web devices: a phone's screen is its own size");
|
|
17070
|
+
}
|
|
17071
|
+
const preset = VIEWPORT_PRESET_NAMES.find((name) => name === value.trim().toLowerCase());
|
|
17072
|
+
if (!preset)
|
|
17073
|
+
throw new Error(`--viewport must be ${VIEWPORT_PRESET_NAMES.join(" or ")}`);
|
|
17074
|
+
return preset;
|
|
17075
|
+
}
|
|
17076
|
+
|
|
16922
17077
|
// src/report.ts
|
|
16923
17078
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
|
|
16924
17079
|
import { dirname as dirname3 } from "node:path";
|
|
@@ -17207,6 +17362,10 @@ program2.name("yoqa").description("Local Yoqa CLI (talks to the local runner ove
|
|
|
17207
17362
|
function client(baseUrl) {
|
|
17208
17363
|
return createRunnerClient({ baseUrl });
|
|
17209
17364
|
}
|
|
17365
|
+
function describeSession(body) {
|
|
17366
|
+
const viewport = body.viewport ? ` viewport ${formatViewportPreset(body.viewport)}` : "";
|
|
17367
|
+
return `${body.platform} ${body.deviceId} lane ${body.lane ?? "direct"}${viewport}`;
|
|
17368
|
+
}
|
|
17210
17369
|
function fail(command, error) {
|
|
17211
17370
|
const message = error instanceof Error ? error.message : String(error);
|
|
17212
17371
|
console.error(`yoqa ${command} failed: ${message}`);
|
|
@@ -17343,8 +17502,8 @@ function printDevicesTable(devicesList) {
|
|
|
17343
17502
|
}
|
|
17344
17503
|
}
|
|
17345
17504
|
}
|
|
17346
|
-
for (const platform of
|
|
17347
|
-
devices.command(platform).description(
|
|
17505
|
+
for (const platform of Object.keys(PLATFORM_DEVICES_DESCRIPTION)) {
|
|
17506
|
+
devices.command(platform).description(PLATFORM_DEVICES_DESCRIPTION[platform]).option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--all", "Include unavailable / shutdown devices", true).option("--booted-only", "Only show booted / online devices").action(async (options) => {
|
|
17348
17507
|
try {
|
|
17349
17508
|
const includeUnavailable = options.bootedOnly ? false : options.all !== false;
|
|
17350
17509
|
const body = await client(options.baseUrl).listDevices(platform, {
|
|
@@ -17360,28 +17519,27 @@ for (const platform of ["ios", "android"]) {
|
|
|
17360
17519
|
}
|
|
17361
17520
|
});
|
|
17362
17521
|
}
|
|
17363
|
-
devices.command("connect").description("Open a Device Session on a device").argument("<deviceId>", "Device UDID / serial").
|
|
17522
|
+
devices.command("connect").description("Open a Device Session on a device").argument("<deviceId>", "Device UDID / serial, a browser id (web-chrome), or chromium-cdp-<port> for a Chromium you started").option("--platform <platform>", "ios | android | web (web for web-* and chromium-cdp-<port>)").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--bundle-id <id>", "iOS bundle id to launch").option("--app-package <id>", "Android application id to launch").option("--lane <lane>", "direct | auto (the only Lane is Direct)").option("--viewport <preset>", "Web only: desktop (1280×800, the default) | mobile (390×844, touch and a mobile user agent)").option("--json", "Print raw JSON").action(async (deviceId, options) => {
|
|
17364
17523
|
try {
|
|
17365
|
-
const platform = options.platform;
|
|
17366
|
-
if (platform !== "ios" && platform !== "android") {
|
|
17367
|
-
throw new Error("--platform must be ios or android");
|
|
17368
|
-
}
|
|
17524
|
+
const platform = connectPlatform(deviceId, options.platform);
|
|
17369
17525
|
const lane = options.lane;
|
|
17370
17526
|
if (lane != null && lane !== "direct" && lane !== "auto") {
|
|
17371
17527
|
throw new Error("--lane must be direct or auto");
|
|
17372
17528
|
}
|
|
17529
|
+
const viewport = parseViewportPreset(options.viewport, platform);
|
|
17373
17530
|
const body = await client(options.baseUrl).connectDevice({
|
|
17374
17531
|
deviceId,
|
|
17375
17532
|
platform,
|
|
17376
17533
|
bundleId: options.bundleId,
|
|
17377
17534
|
appPackage: options.appPackage,
|
|
17378
|
-
lane
|
|
17535
|
+
lane,
|
|
17536
|
+
...viewport ? { viewport } : {}
|
|
17379
17537
|
});
|
|
17380
17538
|
if (options.json) {
|
|
17381
17539
|
console.log(JSON.stringify(body, null, 2));
|
|
17382
17540
|
return;
|
|
17383
17541
|
}
|
|
17384
|
-
console.log(`connected ${body
|
|
17542
|
+
console.log(`connected ${describeSession(body)}`);
|
|
17385
17543
|
if (body.laneWarning)
|
|
17386
17544
|
console.log(`lane warning: ${body.laneWarning}`);
|
|
17387
17545
|
} catch (error) {
|
|
@@ -17406,7 +17564,7 @@ devices.command("active").description("Show the currently connected device").opt
|
|
|
17406
17564
|
console.log(JSON.stringify(body, null, 2));
|
|
17407
17565
|
return;
|
|
17408
17566
|
}
|
|
17409
|
-
console.log(
|
|
17567
|
+
console.log(describeSession(body));
|
|
17410
17568
|
if (body.laneWarning)
|
|
17411
17569
|
console.log(`lane warning: ${body.laneWarning}`);
|
|
17412
17570
|
} catch (error) {
|
|
@@ -17453,7 +17611,7 @@ program2.command("screenshot").description("Capture a screenshot from the active
|
|
|
17453
17611
|
});
|
|
17454
17612
|
var action = program2.command("action").description("Perform an action on the active device");
|
|
17455
17613
|
function addActionOptions(cmd) {
|
|
17456
|
-
return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v)).option("--screenshot", "Wait for the screen to settle, then return the result screenshot (and a copy marking where the tap or swipe landed)").option("--settle <ms>", "With --screenshot: longest wait for the screen to settle (default 1500)", (v) => Number(v)).option("--full", "With --screenshot: return the raw Result screenshot instead of the Agent image").option("--scale <n>", "With --screenshot: Agent image scale 0.01–1 (default: long edge ~1000 px)", (v) => Number(v));
|
|
17614
|
+
return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id, or the Web URL on web").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v)).option("--screenshot", "Wait for the screen to settle, then return the result screenshot (and a copy marking where the tap or swipe landed)").option("--settle <ms>", "With --screenshot: longest wait for the screen to settle (default 1500)", (v) => Number(v)).option("--full", "With --screenshot: return the raw Result screenshot instead of the Agent image").option("--scale <n>", "With --screenshot: Agent image scale 0.01–1 (default: long edge ~1000 px)", (v) => Number(v));
|
|
17457
17615
|
}
|
|
17458
17616
|
function printResultScreenshot(shot) {
|
|
17459
17617
|
console.log(`screenshot ${shot.path}`);
|
|
@@ -17538,6 +17696,32 @@ addActionOptions(action.command("alert").description("Accept or dismiss a system
|
|
|
17538
17696
|
fail("action alert", error);
|
|
17539
17697
|
}
|
|
17540
17698
|
});
|
|
17699
|
+
addActionOptions(action.command("key").description(`Press a key: ${KEY_NAMES.join(", ")}`).argument("<key>", "Key name, in any case").option("--modifiers <list>", `Held while the key is pressed, comma-separated: ${KEY_MODIFIERS.join(", ")}`)).action(async (keyName, options) => {
|
|
17700
|
+
try {
|
|
17701
|
+
const key = parseKeyName(keyName);
|
|
17702
|
+
const modifiers = typeof options.modifiers === "string" ? parseKeyModifiers(options.modifiers) : [];
|
|
17703
|
+
const body = await client(String(options.baseUrl)).performAction({
|
|
17704
|
+
kind: "key",
|
|
17705
|
+
key,
|
|
17706
|
+
...modifiers.length > 0 ? { modifiers } : {},
|
|
17707
|
+
screenshot: options.screenshot === true ? true : undefined,
|
|
17708
|
+
settleMs: options.settle,
|
|
17709
|
+
fullImage: options.full === true ? true : undefined,
|
|
17710
|
+
imageScale: options.scale
|
|
17711
|
+
});
|
|
17712
|
+
if (options.json) {
|
|
17713
|
+
console.log(JSON.stringify(body, null, 2));
|
|
17714
|
+
return;
|
|
17715
|
+
}
|
|
17716
|
+
console.log(`ok ${body.kind}`);
|
|
17717
|
+
if (body.screenshot)
|
|
17718
|
+
printResultScreenshot(body.screenshot);
|
|
17719
|
+
if (body.phases)
|
|
17720
|
+
printActionPhases(body.phases);
|
|
17721
|
+
} catch (error) {
|
|
17722
|
+
fail("action key", error);
|
|
17723
|
+
}
|
|
17724
|
+
});
|
|
17541
17725
|
addActionOptions(action.command("batch").description("Run known Actions from a JSON file or stdin (one Settle and Result screenshot at the end). Description-grounded steps are rejected.").argument("[file]", "JSON file of { steps: Action[] } or an Action array. Omit to read stdin.")).action(async (file, options) => {
|
|
17542
17726
|
try {
|
|
17543
17727
|
const raw = file ? await readFile2(file, "utf8") : await new Promise((resolve, reject) => {
|
|
@@ -17741,29 +17925,43 @@ signing.command("check").description("Sign YoqaRunner for a cabled iPhone now; r
|
|
|
17741
17925
|
fail("signing check", error);
|
|
17742
17926
|
}
|
|
17743
17927
|
});
|
|
17744
|
-
var assertCmd = program2.command("assert").description("Assert text visibility on the active device");
|
|
17928
|
+
var assertCmd = program2.command("assert").description("Assert text or id visibility on the active device");
|
|
17745
17929
|
for (const kind of ["visible", "not-visible"]) {
|
|
17746
|
-
assertCmd.command(kind).description(kind === "visible" ? "Fail unless matching text appears in the cleaned screen tree" : "Fail if matching text is still present in the cleaned screen tree").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").
|
|
17930
|
+
assertCmd.command(kind).description(kind === "visible" ? "Fail unless matching text or id appears in the cleaned screen tree" : "Fail if matching text or id is still present in the cleaned screen tree").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-t, --text <text>", "Text / label substring to match").option("--id <id>", "Element id to match, as a tap by id does (instead of --text)").option("--timeout <seconds>", "Seconds to wait before failing", (v) => Number(v), 5).action(async (options) => {
|
|
17931
|
+
if (options.text == null === (options.id == null)) {
|
|
17932
|
+
const usage = options.text != null ? "pass --text or --id, not both" : "pass --text <text> or --id <id>";
|
|
17933
|
+
console.error(`yoqa assert ${kind} failed: ${usage}`);
|
|
17934
|
+
process.exitCode = ASSERT_EXIT_CODE.couldNotCheck;
|
|
17935
|
+
return;
|
|
17936
|
+
}
|
|
17937
|
+
const check = {
|
|
17938
|
+
assertion: kind,
|
|
17939
|
+
...options.id != null ? { id: options.id } : { text: options.text },
|
|
17940
|
+
timeoutMs: options.timeout * 1000
|
|
17941
|
+
};
|
|
17747
17942
|
try {
|
|
17748
|
-
const
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17943
|
+
const c = client(options.baseUrl);
|
|
17944
|
+
const { met } = await checkScreen({
|
|
17945
|
+
...check,
|
|
17946
|
+
readScreen: () => c.getScreen(),
|
|
17947
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
17948
|
+
now: Date.now
|
|
17752
17949
|
});
|
|
17753
|
-
const
|
|
17950
|
+
const error = met ? undefined : screenCheckMissMessage(check);
|
|
17951
|
+
if (!met)
|
|
17952
|
+
process.exitCode = ASSERT_EXIT_CODE.notMet;
|
|
17754
17953
|
if (options.json) {
|
|
17755
|
-
console.log(JSON.stringify(
|
|
17756
|
-
if (!result.ok)
|
|
17757
|
-
process.exitCode = 1;
|
|
17954
|
+
console.log(JSON.stringify({ ok: met, completed: met ? 1 : 0, total: 1, error }, null, 2));
|
|
17758
17955
|
return;
|
|
17759
17956
|
}
|
|
17760
|
-
if (
|
|
17761
|
-
|
|
17957
|
+
if (error) {
|
|
17958
|
+
console.error(`yoqa assert ${kind} failed: ${error}`);
|
|
17762
17959
|
return;
|
|
17763
17960
|
}
|
|
17764
17961
|
console.log(`ok assert ${kind}`);
|
|
17765
17962
|
} catch (error) {
|
|
17766
17963
|
fail(`assert ${kind}`, error);
|
|
17964
|
+
process.exitCode = ASSERT_EXIT_CODE.couldNotCheck;
|
|
17767
17965
|
}
|
|
17768
17966
|
});
|
|
17769
17967
|
}
|
|
@@ -17783,10 +17981,14 @@ appsCmd.command("list").option("--base-url <url>", "Runner base URL", runnerBase
|
|
|
17783
17981
|
fail("apps list", error);
|
|
17784
17982
|
}
|
|
17785
17983
|
});
|
|
17786
|
-
appsCmd.command("create").description("Create a local catalog app").requiredOption("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix (allocated from name when omitted)").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (options) => {
|
|
17984
|
+
appsCmd.command("create").description("Create a local catalog app").requiredOption("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix (allocated from name when omitted)").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--web-url <url>", "Web URL a Run on web opens (http or https)").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").action(async (options) => {
|
|
17787
17985
|
try {
|
|
17788
17986
|
const c = client(options.baseUrl);
|
|
17789
|
-
let app = await c.createApp({
|
|
17987
|
+
let app = await c.createApp({
|
|
17988
|
+
name: options.name,
|
|
17989
|
+
prefix: options.prefix,
|
|
17990
|
+
webUrl: options.webUrl
|
|
17991
|
+
});
|
|
17790
17992
|
if (options.iosBundleId || options.androidApplicationId) {
|
|
17791
17993
|
app = await c.updateApp(app.id, {
|
|
17792
17994
|
iosBundleId: options.iosBundleId,
|
|
@@ -17817,11 +18019,12 @@ appsCmd.command("get").argument("<app>", "App prefix or id").option("--base-url
|
|
|
17817
18019
|
console.log(`app_knowledge: ${app.knowledge || "(empty)"}`);
|
|
17818
18020
|
console.log(`ios_bundle_id: ${app.iosBundleId || "(empty)"}`);
|
|
17819
18021
|
console.log(`android_application_id: ${app.androidApplicationId || "(empty)"}`);
|
|
18022
|
+
console.log(`web_url: ${app.webUrl || "(empty)"}`);
|
|
17820
18023
|
} catch (error) {
|
|
17821
18024
|
fail("apps get", error);
|
|
17822
18025
|
}
|
|
17823
18026
|
});
|
|
17824
|
-
appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix").option("--context <text>", "App context / shared rules").option("--knowledge <text>", "App knowledge notes injected into agent runs").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
18027
|
+
appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix").option("--context <text>", "App context / shared rules").option("--knowledge <text>", "App knowledge notes injected into agent runs").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--web-url <url>", "Web URL a Run on web opens (empty clears it)").option("--json", "Print raw JSON").action(async (appArg, options) => {
|
|
17825
18028
|
try {
|
|
17826
18029
|
const c = client(options.baseUrl);
|
|
17827
18030
|
const resolved = await resolveAppId(c, appArg);
|
|
@@ -17831,7 +18034,8 @@ appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-u
|
|
|
17831
18034
|
context: options.context,
|
|
17832
18035
|
knowledge: options.knowledge,
|
|
17833
18036
|
iosBundleId: options.iosBundleId,
|
|
17834
|
-
androidApplicationId: options.androidApplicationId
|
|
18037
|
+
androidApplicationId: options.androidApplicationId,
|
|
18038
|
+
webUrl: options.webUrl
|
|
17835
18039
|
});
|
|
17836
18040
|
if (options.json) {
|
|
17837
18041
|
console.log(JSON.stringify(app, null, 2));
|
|
@@ -18155,7 +18359,7 @@ runsCmd.command("delete").argument("<runId>", "Run id").option("--base-url <url>
|
|
|
18155
18359
|
fail("runs delete", error);
|
|
18156
18360
|
}
|
|
18157
18361
|
});
|
|
18158
|
-
runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android (defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--screen-mode <mode>", "vision | tree — what the agent sees: screenshot only, or also the accessibility tree (default: vision)").option("--lane <lane>", "direct | auto").option("--record-video", "Record a screen video of every case in the run, even cases not set to record").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
|
|
18362
|
+
runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android | web (web for web-* and chromium-cdp-<port>; defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--screen-mode <mode>", "vision | tree — what the agent sees: screenshot only, or also the accessibility tree (default: vision)").option("--lane <lane>", "direct | auto").option("--record-video", "Record a screen video of every case in the run, even cases not set to record").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
|
|
18159
18363
|
try {
|
|
18160
18364
|
const c = client(options.baseUrl);
|
|
18161
18365
|
const resolved = await resolveAppId(c, appArg);
|
|
@@ -18171,18 +18375,16 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
|
|
|
18171
18375
|
caseIds.push(found.id);
|
|
18172
18376
|
}
|
|
18173
18377
|
let deviceId = options.device;
|
|
18174
|
-
let
|
|
18175
|
-
if (!deviceId || !
|
|
18378
|
+
let platformOption = options.platform ?? (deviceId && isWebDeviceId(deviceId) ? "web" : undefined);
|
|
18379
|
+
if (!deviceId || !platformOption) {
|
|
18176
18380
|
const active = await c.getActiveDevice();
|
|
18177
18381
|
if (!active) {
|
|
18178
18382
|
throw new Error("No --device/--platform and no active session. Connect first or pass both flags.");
|
|
18179
18383
|
}
|
|
18180
18384
|
deviceId = deviceId ?? active.deviceId;
|
|
18181
|
-
|
|
18182
|
-
}
|
|
18183
|
-
if (platform !== "ios" && platform !== "android") {
|
|
18184
|
-
throw new Error("--platform must be ios or android");
|
|
18385
|
+
platformOption = platformOption ?? active.platform;
|
|
18185
18386
|
}
|
|
18387
|
+
const platform = parsePlatform(platformOption);
|
|
18186
18388
|
const executionMode = options.mode === "script" || options.mode === "agent" || options.mode === "auto" ? options.mode : undefined;
|
|
18187
18389
|
if (options.mode && !executionMode) {
|
|
18188
18390
|
throw new Error("--mode must be auto, script, or agent");
|
|
@@ -18269,23 +18471,26 @@ scriptCmd.command("run").argument("<file>", "Path to a Case Script file (.yoqa.y
|
|
|
18269
18471
|
}
|
|
18270
18472
|
const result = await runCaseScript(c, script, {
|
|
18271
18473
|
pauseAfterActionMs: 800,
|
|
18272
|
-
onStep: (
|
|
18474
|
+
onStep: (event) => {
|
|
18475
|
+
const { position, total, action: action2, status, error, response, outcome } = event;
|
|
18273
18476
|
if (status === "running")
|
|
18274
18477
|
return;
|
|
18275
|
-
const step =
|
|
18478
|
+
const step = position + 1;
|
|
18276
18479
|
if (options.json) {
|
|
18277
18480
|
console.log(JSON.stringify({
|
|
18278
18481
|
ok: status === "ok",
|
|
18279
18482
|
status,
|
|
18280
18483
|
type: action2.type,
|
|
18281
18484
|
step,
|
|
18485
|
+
path: event.path,
|
|
18282
18486
|
command: caseScriptActionCommand(action2),
|
|
18487
|
+
...outcome ? { outcome } : {},
|
|
18283
18488
|
...error ? { error } : {},
|
|
18284
18489
|
...response ? { response } : {}
|
|
18285
18490
|
}, null, 2));
|
|
18286
18491
|
return;
|
|
18287
18492
|
}
|
|
18288
|
-
const line =
|
|
18493
|
+
const line = formatCaseScriptReplayStep(event);
|
|
18289
18494
|
if (status === "ok")
|
|
18290
18495
|
console.log(`ok ${line} (${step}/${total})`);
|
|
18291
18496
|
else if (status === "error")
|
|
@@ -18295,10 +18500,11 @@ scriptCmd.command("run").argument("<file>", "Path to a Case Script file (.yoqa.y
|
|
|
18295
18500
|
}
|
|
18296
18501
|
});
|
|
18297
18502
|
if (!result.ok) {
|
|
18298
|
-
|
|
18503
|
+
const where = result.failedPath ? formatCaseScriptStepPath(result.failedPath) : `action ${(result.failedIndex ?? 0) + 1}`;
|
|
18504
|
+
throw new Error(`${where} failed: ${result.error ?? "failed"}`);
|
|
18299
18505
|
}
|
|
18300
18506
|
if (!options.json) {
|
|
18301
|
-
console.log(`done ${result.total}
|
|
18507
|
+
console.log(`done ${result.total} step${result.total === 1 ? "" : "s"}`);
|
|
18302
18508
|
}
|
|
18303
18509
|
} catch (error) {
|
|
18304
18510
|
fail("script run", error);
|
|
@@ -18335,10 +18541,9 @@ scriptCmd.command("export").argument("<app>", "App prefix or id").argument("<num
|
|
|
18335
18541
|
});
|
|
18336
18542
|
program2.command("benchmark").description("Compare Yoqa (and Argent, when installed) tap-to-result latency on one device").requiredOption("--device <id>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--repeats <n>", "Tap-to-result repeats (default: suite repeats)", (v) => Number(v)).option("--suite <path>", "Versioned suite JSON (default: built-in v1)").option("--tools <list>", "Comma-separated: yoqa,argent (default: yoqa)", "yoqa").option("--lane <lane>", "Yoqa Lane: direct | auto", "auto").option("--arms <list>", "Yoqa Screen arms: vision,tree (default: suite arms)").option("--app <id>", "Catalog App id for Case arms (required when the suite has cases)").option("--out <path>", "Write the JSON report to this path").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print JSON instead of the table").action(async (options) => {
|
|
18337
18543
|
try {
|
|
18338
|
-
const platform = options.platform;
|
|
18339
|
-
if (platform
|
|
18340
|
-
throw new Error("
|
|
18341
|
-
}
|
|
18544
|
+
const platform = parsePlatform(options.platform);
|
|
18545
|
+
if (platform === "web")
|
|
18546
|
+
throw new Error("benchmark runs on ios or android only");
|
|
18342
18547
|
const lane = options.lane === "direct" || options.lane === "auto" ? options.lane : undefined;
|
|
18343
18548
|
if (options.lane && !lane)
|
|
18344
18549
|
throw new Error("--lane must be direct or auto");
|