@testrelic/maestro-analytics 1.1.0 → 1.2.0-next.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/cli.cjs +1131 -165
- package/dist/index.cjs +66 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -4
- package/dist/index.d.ts +108 -4
- package/dist/index.js +66 -46
- package/dist/index.js.map +1 -1
- package/dist/merge.cjs +2 -2
- package/dist/merge.cjs.map +1 -1
- package/dist/merge.js +2 -2
- package/dist/merge.js.map +1 -1
- package/dist/proxy-addon/testrelic_capture.py +217 -0
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -190,6 +190,37 @@ function mergeCloudConfig(fileConfig, reporterOptions) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// src/config.ts
|
|
193
|
+
var DEFAULT_REDACT_HEADERS = [
|
|
194
|
+
"authorization",
|
|
195
|
+
"cookie",
|
|
196
|
+
"set-cookie",
|
|
197
|
+
"x-api-key"
|
|
198
|
+
];
|
|
199
|
+
var DEFAULT_REDACT_BODY_FIELDS = [
|
|
200
|
+
"password",
|
|
201
|
+
"secret",
|
|
202
|
+
"token",
|
|
203
|
+
"apiKey",
|
|
204
|
+
"api_key"
|
|
205
|
+
];
|
|
206
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
207
|
+
function resolveNetworkCapture(options) {
|
|
208
|
+
const o = options ?? {};
|
|
209
|
+
const envEnabled = process.env.TESTRELIC_CAPTURE_NETWORK === "1" || process.env.TESTRELIC_CAPTURE_NETWORK?.toLowerCase() === "true";
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
enabled: o.enabled ?? envEnabled ?? false,
|
|
212
|
+
proxyPort: o.proxyPort ?? (process.env.TESTRELIC_PROXY_PORT ? Number.parseInt(process.env.TESTRELIC_PROXY_PORT, 10) : 8080),
|
|
213
|
+
proxyHost: o.proxyHost ?? process.env.TESTRELIC_PROXY_HOST ?? "127.0.0.1",
|
|
214
|
+
harPath: o.harPath ?? process.env.TESTRELIC_HAR_PATH ?? null,
|
|
215
|
+
outputDir: o.outputDir ?? null,
|
|
216
|
+
skipCertInstall: o.skipCertInstall ?? false,
|
|
217
|
+
includeUrls: Object.freeze(o.includeUrls ? [...o.includeUrls] : []),
|
|
218
|
+
excludeUrls: Object.freeze(o.excludeUrls ? [...o.excludeUrls] : []),
|
|
219
|
+
redactHeaders: Object.freeze(o.redactHeaders ? [...o.redactHeaders] : [...DEFAULT_REDACT_HEADERS]),
|
|
220
|
+
redactBodyFields: Object.freeze(o.redactBodyFields ? [...o.redactBodyFields] : [...DEFAULT_REDACT_BODY_FIELDS]),
|
|
221
|
+
maxBodyBytes: o.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
222
|
+
});
|
|
223
|
+
}
|
|
193
224
|
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
194
225
|
function hasPrototypePollution(obj) {
|
|
195
226
|
if (typeof obj !== "object" || obj === null) return false;
|
|
@@ -237,6 +268,7 @@ function resolveConfig(options) {
|
|
|
237
268
|
target.quiet = options?.quiet ?? false;
|
|
238
269
|
target.reportMode = options?.reportMode ?? "batch";
|
|
239
270
|
target.cloud = resolveCloudFromMerge(options?.cloud ?? null);
|
|
271
|
+
target.network = resolveNetworkCapture(options?.network);
|
|
240
272
|
return Object.freeze(target);
|
|
241
273
|
}
|
|
242
274
|
function resolveCloudFromMerge(reporterOptions) {
|
|
@@ -251,14 +283,323 @@ function resolveCloudFromMerge(reporterOptions) {
|
|
|
251
283
|
}
|
|
252
284
|
|
|
253
285
|
// src/maestro-runner.ts
|
|
286
|
+
var import_node_child_process3 = require("child_process");
|
|
287
|
+
var import_node_fs4 = require("fs");
|
|
288
|
+
var import_node_path4 = require("path");
|
|
289
|
+
var import_node_os3 = require("os");
|
|
290
|
+
var import_node_crypto2 = require("crypto");
|
|
291
|
+
|
|
292
|
+
// src/network-proxy.ts
|
|
254
293
|
var import_node_child_process = require("child_process");
|
|
255
294
|
var import_node_fs2 = require("fs");
|
|
256
295
|
var import_node_path2 = require("path");
|
|
296
|
+
var import_node_url = require("url");
|
|
257
297
|
var import_node_os = require("os");
|
|
258
298
|
var import_node_crypto = require("crypto");
|
|
299
|
+
var import_meta = {};
|
|
300
|
+
function resolveAddonPath() {
|
|
301
|
+
const candidates = [];
|
|
302
|
+
try {
|
|
303
|
+
const here = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
304
|
+
candidates.push((0, import_node_path2.join)(here, "proxy-addon", "testrelic_capture.py"));
|
|
305
|
+
candidates.push((0, import_node_path2.join)(here, "..", "src", "proxy-addon", "testrelic_capture.py"));
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
const maybeDirname = globalThis.__dirname;
|
|
309
|
+
if (maybeDirname) {
|
|
310
|
+
candidates.push((0, import_node_path2.join)(maybeDirname, "proxy-addon", "testrelic_capture.py"));
|
|
311
|
+
}
|
|
312
|
+
for (const c of candidates) {
|
|
313
|
+
if ((0, import_node_fs2.existsSync)(c)) return (0, import_node_path2.resolve)(c);
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function generateRunDir(base) {
|
|
318
|
+
if (base) {
|
|
319
|
+
(0, import_node_fs2.mkdirSync)(base, { recursive: true });
|
|
320
|
+
return base;
|
|
321
|
+
}
|
|
322
|
+
const id = (0, import_node_crypto.randomBytes)(6).toString("hex");
|
|
323
|
+
const dir = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `testrelic-maestro-network-${id}`);
|
|
324
|
+
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
325
|
+
return dir;
|
|
326
|
+
}
|
|
327
|
+
async function isMitmdumpAvailable() {
|
|
328
|
+
return new Promise((resolvePromise) => {
|
|
329
|
+
const child = (0, import_node_child_process.spawn)("mitmdump", ["--version"], { shell: true });
|
|
330
|
+
let resolved = false;
|
|
331
|
+
const settle = (ok) => {
|
|
332
|
+
if (resolved) return;
|
|
333
|
+
resolved = true;
|
|
334
|
+
resolvePromise(ok);
|
|
335
|
+
};
|
|
336
|
+
child.on("error", () => settle(false));
|
|
337
|
+
child.on("close", (code) => settle(code === 0));
|
|
338
|
+
setTimeout(() => {
|
|
339
|
+
try {
|
|
340
|
+
child.kill();
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
settle(false);
|
|
344
|
+
}, 5e3);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async function startNetworkProxy(options) {
|
|
348
|
+
const { config, verbose = true } = options;
|
|
349
|
+
if (!config.enabled) return null;
|
|
350
|
+
if (!await isMitmdumpAvailable()) {
|
|
351
|
+
if (verbose) {
|
|
352
|
+
process.stderr.write(
|
|
353
|
+
"\u26A0 TestRelic: --capture-network requested but `mitmdump` is not on PATH.\n Install mitmproxy (`brew install mitmproxy` / `pipx install mitmproxy`)\n or supply --har-path to import a HAR file instead.\n"
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const addon = resolveAddonPath();
|
|
359
|
+
if (!addon) {
|
|
360
|
+
if (verbose) {
|
|
361
|
+
process.stderr.write("\u26A0 TestRelic: network-capture addon not found in package; skipping capture.\n");
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const outputDir = generateRunDir(options.outputDir ?? config.outputDir ?? void 0);
|
|
366
|
+
const outputPath = (0, import_node_path2.join)(outputDir, "network.jsonl");
|
|
367
|
+
const args = [
|
|
368
|
+
"--listen-host",
|
|
369
|
+
config.proxyHost,
|
|
370
|
+
"--listen-port",
|
|
371
|
+
String(config.proxyPort),
|
|
372
|
+
"-s",
|
|
373
|
+
addon,
|
|
374
|
+
"--set",
|
|
375
|
+
`testrelic_output=${outputPath}`,
|
|
376
|
+
"--set",
|
|
377
|
+
`testrelic_redact_headers=${config.redactHeaders.join(",")}`,
|
|
378
|
+
"--set",
|
|
379
|
+
`testrelic_redact_body_fields=${config.redactBodyFields.join(",")}`,
|
|
380
|
+
"--set",
|
|
381
|
+
`testrelic_max_body_bytes=${config.maxBodyBytes}`,
|
|
382
|
+
// Reduce noise; the addon writes its own log lines on warn.
|
|
383
|
+
"--set",
|
|
384
|
+
"console_eventlog_verbosity=warn",
|
|
385
|
+
"--set",
|
|
386
|
+
"termlog_verbosity=warn"
|
|
387
|
+
];
|
|
388
|
+
let child;
|
|
389
|
+
try {
|
|
390
|
+
child = (0, import_node_child_process.spawn)("mitmdump", args, {
|
|
391
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
392
|
+
shell: true
|
|
393
|
+
});
|
|
394
|
+
} catch (err) {
|
|
395
|
+
if (verbose) {
|
|
396
|
+
process.stderr.write(`\u26A0 TestRelic: failed to spawn mitmdump: ${err.message}
|
|
397
|
+
`);
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
child.stdout?.on("data", (chunk) => {
|
|
402
|
+
if (verbose) process.stderr.write(`[mitmdump] ${chunk.toString("utf-8")}`);
|
|
403
|
+
});
|
|
404
|
+
child.stderr?.on("data", (chunk) => {
|
|
405
|
+
if (verbose) process.stderr.write(`[mitmdump] ${chunk.toString("utf-8")}`);
|
|
406
|
+
});
|
|
407
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
408
|
+
const stop = () => new Promise((resolvePromise) => {
|
|
409
|
+
if (child.killed || child.exitCode !== null) {
|
|
410
|
+
resolvePromise();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
let settled = false;
|
|
414
|
+
const settle = () => {
|
|
415
|
+
if (settled) return;
|
|
416
|
+
settled = true;
|
|
417
|
+
resolvePromise();
|
|
418
|
+
};
|
|
419
|
+
child.once("close", settle);
|
|
420
|
+
child.once("exit", settle);
|
|
421
|
+
try {
|
|
422
|
+
child.kill("SIGINT");
|
|
423
|
+
} catch {
|
|
424
|
+
try {
|
|
425
|
+
child.kill();
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
if (!settled) {
|
|
431
|
+
try {
|
|
432
|
+
child.kill("SIGKILL");
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
settle();
|
|
436
|
+
}
|
|
437
|
+
}, 4e3);
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
outputPath,
|
|
441
|
+
outputDir,
|
|
442
|
+
host: config.proxyHost,
|
|
443
|
+
port: config.proxyPort,
|
|
444
|
+
stop
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/device-proxy-setup.ts
|
|
449
|
+
var import_node_child_process2 = require("child_process");
|
|
450
|
+
var import_node_path3 = require("path");
|
|
451
|
+
var import_node_os2 = require("os");
|
|
452
|
+
var import_node_fs3 = require("fs");
|
|
453
|
+
var NOOP_HANDLE = { teardown: async () => {
|
|
454
|
+
} };
|
|
455
|
+
function commandExists(cmd) {
|
|
456
|
+
const result = (0, import_node_child_process2.spawnSync)(cmd, ["--version"], { shell: true, stdio: "ignore" });
|
|
457
|
+
return result.error === void 0 || result.error.code !== "ENOENT";
|
|
458
|
+
}
|
|
459
|
+
function logHint(quiet, msg) {
|
|
460
|
+
if (!quiet) process.stderr.write(`${msg}
|
|
461
|
+
`);
|
|
462
|
+
}
|
|
463
|
+
function logManualInstructions(quiet, platform, proxyHost, proxyPort) {
|
|
464
|
+
if (quiet) return;
|
|
465
|
+
if (platform === "android") {
|
|
466
|
+
process.stderr.write(
|
|
467
|
+
`\u2139 TestRelic: real-device Android capture \u2014 set the device proxy manually:
|
|
468
|
+
Settings \u2192 Wi-Fi \u2192 (long-press your network) \u2192 Modify \u2192 Proxy: Manual
|
|
469
|
+
Host: ${proxyHost} Port: ${proxyPort}
|
|
470
|
+
Then install mitmproxy CA: open http://mitm.it from the device after the proxy is reachable.
|
|
471
|
+
`
|
|
472
|
+
);
|
|
473
|
+
} else if (platform === "ios") {
|
|
474
|
+
process.stderr.write(
|
|
475
|
+
`\u2139 TestRelic: real-device iOS capture \u2014 set the device proxy manually:
|
|
476
|
+
Settings \u2192 Wi-Fi \u2192 (i) on your network \u2192 Configure Proxy \u2192 Manual
|
|
477
|
+
Server: ${proxyHost} Port: ${proxyPort}
|
|
478
|
+
Then install + TRUST the mitmproxy CA:
|
|
479
|
+
1. Open http://mitm.it from Safari on the device \u2192 install profile.
|
|
480
|
+
2. Settings \u2192 General \u2192 About \u2192 Certificate Trust Settings \u2192 enable mitmproxy.
|
|
481
|
+
`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async function adb(args, deviceId) {
|
|
486
|
+
return new Promise((resolvePromise) => {
|
|
487
|
+
const full = deviceId ? ["-s", deviceId, ...args] : args;
|
|
488
|
+
const child = (0, import_node_child_process2.spawn)("adb", full, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
489
|
+
let out = "";
|
|
490
|
+
let err = "";
|
|
491
|
+
child.stdout?.on("data", (c) => {
|
|
492
|
+
out += c.toString();
|
|
493
|
+
});
|
|
494
|
+
child.stderr?.on("data", (c) => {
|
|
495
|
+
err += c.toString();
|
|
496
|
+
});
|
|
497
|
+
child.on("error", () => resolvePromise({ code: 127, stdout: out, stderr: err || "adb not on PATH" }));
|
|
498
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout: out, stderr: err }));
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
async function setUpAndroidEmulator(opts) {
|
|
502
|
+
const { deviceId, proxyHost, proxyPort, quiet, skipCertInstall } = opts;
|
|
503
|
+
if (!commandExists("adb")) {
|
|
504
|
+
logHint(quiet, "\u26A0 TestRelic: `adb` not on PATH; skipping automatic Android proxy setup. See manual instructions:");
|
|
505
|
+
logManualInstructions(quiet, "android", proxyHost, proxyPort);
|
|
506
|
+
return NOOP_HANDLE;
|
|
507
|
+
}
|
|
508
|
+
const setResult = await adb(["shell", "settings", "put", "global", "http_proxy", `${proxyHost}:${proxyPort}`], deviceId);
|
|
509
|
+
if (setResult.code !== 0) {
|
|
510
|
+
logHint(quiet, `\u26A0 TestRelic: adb set proxy failed: ${setResult.stderr.trim() || "unknown error"}`);
|
|
511
|
+
logManualInstructions(quiet, "android", proxyHost, proxyPort);
|
|
512
|
+
return NOOP_HANDLE;
|
|
513
|
+
}
|
|
514
|
+
logHint(quiet, `\u2139 TestRelic: Android proxy set to ${proxyHost}:${proxyPort} on ${deviceId ?? "default device"}.`);
|
|
515
|
+
if (!skipCertInstall) {
|
|
516
|
+
logHint(
|
|
517
|
+
quiet,
|
|
518
|
+
"\u2139 TestRelic: if HTTPS calls fail, install the mitmproxy CA on the emulator:\n open http://mitm.it from Chrome on the emulator and follow prompts,\n OR for a system-level install:\n adb root && adb remount && adb push <mitm-ca>.0 /system/etc/security/cacerts/ && adb reboot\n Set --skip-cert-install to silence this hint."
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const teardown = async () => {
|
|
522
|
+
await adb(["shell", "settings", "put", "global", "http_proxy", ":0"], deviceId);
|
|
523
|
+
};
|
|
524
|
+
return { teardown };
|
|
525
|
+
}
|
|
526
|
+
async function xcrun(args) {
|
|
527
|
+
return new Promise((resolvePromise) => {
|
|
528
|
+
const child = (0, import_node_child_process2.spawn)("xcrun", args, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
529
|
+
let out = "";
|
|
530
|
+
let err = "";
|
|
531
|
+
child.stdout?.on("data", (c) => {
|
|
532
|
+
out += c.toString();
|
|
533
|
+
});
|
|
534
|
+
child.stderr?.on("data", (c) => {
|
|
535
|
+
err += c.toString();
|
|
536
|
+
});
|
|
537
|
+
child.on("error", () => resolvePromise({ code: 127, stdout: out, stderr: err || "xcrun not on PATH" }));
|
|
538
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout: out, stderr: err }));
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
async function setUpIosSimulator(opts) {
|
|
542
|
+
const { proxyHost, proxyPort, quiet, skipCertInstall } = opts;
|
|
543
|
+
if (process.platform !== "darwin") {
|
|
544
|
+
logHint(quiet, "\u26A0 TestRelic: iOS simulator capture requires macOS; the host is not Darwin. Skipping setup.");
|
|
545
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
546
|
+
return NOOP_HANDLE;
|
|
547
|
+
}
|
|
548
|
+
if (!commandExists("xcrun")) {
|
|
549
|
+
logHint(quiet, "\u26A0 TestRelic: `xcrun` not on PATH; skipping automatic iOS simulator setup.");
|
|
550
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
551
|
+
return NOOP_HANDLE;
|
|
552
|
+
}
|
|
553
|
+
if (!skipCertInstall) {
|
|
554
|
+
const caCandidate = (0, import_node_path3.join)(process.env.HOME ?? "", ".mitmproxy", "mitmproxy-ca-cert.pem");
|
|
555
|
+
if ((0, import_node_fs3.existsSync)(caCandidate)) {
|
|
556
|
+
const udid = opts.deviceId ?? "booted";
|
|
557
|
+
const res = await xcrun(["simctl", "keychain", udid, "add-root-cert", caCandidate]);
|
|
558
|
+
if (res.code === 0) {
|
|
559
|
+
logHint(quiet, `\u2139 TestRelic: installed mitmproxy CA on iOS simulator (${udid}).`);
|
|
560
|
+
} else {
|
|
561
|
+
logHint(quiet, `\u26A0 TestRelic: failed to add-root-cert via simctl: ${res.stderr.trim()}`);
|
|
562
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
logHint(
|
|
566
|
+
quiet,
|
|
567
|
+
`\u26A0 TestRelic: mitmproxy CA not found at ${caCandidate}. Run mitmdump once to generate it,
|
|
568
|
+
then re-run with --capture-network. Falling back to manual instructions:`
|
|
569
|
+
);
|
|
570
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
logHint(
|
|
574
|
+
quiet,
|
|
575
|
+
`\u2139 TestRelic: in the iOS simulator, configure Wi-Fi \u2192 (i) \u2192 Manual proxy:
|
|
576
|
+
Server: ${proxyHost} Port: ${proxyPort}
|
|
577
|
+
(We do not set host-wide \`networksetup\` proxy automatically; see docs.)`
|
|
578
|
+
);
|
|
579
|
+
return NOOP_HANDLE;
|
|
580
|
+
}
|
|
581
|
+
async function setUpDeviceProxy(options) {
|
|
582
|
+
switch (options.platform) {
|
|
583
|
+
case "android":
|
|
584
|
+
return setUpAndroidEmulator(options);
|
|
585
|
+
case "ios":
|
|
586
|
+
return setUpIosSimulator(options);
|
|
587
|
+
case "web":
|
|
588
|
+
logHint(options.quiet, "\u2139 TestRelic: --capture-network ignored for Maestro web flows (browser already handles HTTP).");
|
|
589
|
+
return NOOP_HANDLE;
|
|
590
|
+
case "unknown":
|
|
591
|
+
default:
|
|
592
|
+
logHint(options.quiet, "\u26A0 TestRelic: target platform unknown; printing manual proxy setup instructions:");
|
|
593
|
+
logManualInstructions(options.quiet, "android", options.proxyHost, options.proxyPort);
|
|
594
|
+
logManualInstructions(options.quiet, "ios", options.proxyHost, options.proxyPort);
|
|
595
|
+
return NOOP_HANDLE;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/maestro-runner.ts
|
|
259
600
|
function generateTempDir() {
|
|
260
|
-
const id = (0,
|
|
261
|
-
return (0,
|
|
601
|
+
const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
|
|
602
|
+
return (0, import_node_path4.join)((0, import_node_os3.tmpdir)(), `testrelic-maestro-${id}`);
|
|
262
603
|
}
|
|
263
604
|
function buildMaestroArgs(options, tempDir) {
|
|
264
605
|
const args = [];
|
|
@@ -269,9 +610,9 @@ function buildMaestroArgs(options, tempDir) {
|
|
|
269
610
|
args.push("--device", options.device);
|
|
270
611
|
}
|
|
271
612
|
args.push("test");
|
|
272
|
-
const junitPath = (0,
|
|
273
|
-
const testOutputDir = (0,
|
|
274
|
-
const debugOutputDir = (0,
|
|
613
|
+
const junitPath = (0, import_node_path4.join)(tempDir, "report.xml");
|
|
614
|
+
const testOutputDir = (0, import_node_path4.join)(tempDir, "artifacts");
|
|
615
|
+
const debugOutputDir = (0, import_node_path4.join)(tempDir, "debug");
|
|
275
616
|
args.push("--format", "junit");
|
|
276
617
|
args.push("--output", junitPath);
|
|
277
618
|
args.push("--test-output-dir", testOutputDir);
|
|
@@ -302,60 +643,124 @@ function buildMaestroArgs(options, tempDir) {
|
|
|
302
643
|
args.push(...options.flowPaths);
|
|
303
644
|
return args;
|
|
304
645
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
646
|
+
function normalisePlatform(p) {
|
|
647
|
+
switch ((p ?? "").toLowerCase()) {
|
|
648
|
+
case "android":
|
|
649
|
+
return "android";
|
|
650
|
+
case "ios":
|
|
651
|
+
return "ios";
|
|
652
|
+
case "web":
|
|
653
|
+
return "web";
|
|
654
|
+
default:
|
|
655
|
+
return "unknown";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function runMaestroProc(args, junitPath, testOutputDir, debugOutputDir, networkJsonlDir, quiet, onClose) {
|
|
313
659
|
return new Promise((resolvePromise) => {
|
|
314
660
|
const stdoutChunks = [];
|
|
315
661
|
const stderrChunks = [];
|
|
316
|
-
const proc = (0,
|
|
662
|
+
const proc = (0, import_node_child_process3.spawn)("maestro", args, {
|
|
317
663
|
stdio: ["inherit", "pipe", "pipe"],
|
|
318
664
|
shell: true
|
|
319
665
|
});
|
|
320
666
|
proc.stdout.on("data", (chunk) => {
|
|
321
667
|
stdoutChunks.push(chunk);
|
|
322
|
-
if (!
|
|
668
|
+
if (!quiet) process.stdout.write(chunk);
|
|
323
669
|
});
|
|
324
670
|
proc.stderr.on("data", (chunk) => {
|
|
325
671
|
stderrChunks.push(chunk);
|
|
326
|
-
if (!
|
|
672
|
+
if (!quiet) process.stderr.write(chunk);
|
|
327
673
|
});
|
|
328
|
-
proc.on("close", (code) => {
|
|
674
|
+
proc.on("close", async (code) => {
|
|
675
|
+
try {
|
|
676
|
+
await onClose();
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
329
679
|
resolvePromise({
|
|
330
680
|
exitCode: code ?? 1,
|
|
331
681
|
junitPath,
|
|
332
682
|
testOutputDir,
|
|
333
683
|
debugOutputDir,
|
|
334
684
|
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
335
|
-
stderr: Buffer.concat(stderrChunks).toString("utf-8")
|
|
685
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
686
|
+
networkJsonlDir
|
|
336
687
|
});
|
|
337
688
|
});
|
|
338
|
-
proc.on("error", (err) => {
|
|
689
|
+
proc.on("error", async (err) => {
|
|
690
|
+
try {
|
|
691
|
+
await onClose();
|
|
692
|
+
} catch {
|
|
693
|
+
}
|
|
339
694
|
resolvePromise({
|
|
340
695
|
exitCode: 127,
|
|
341
696
|
junitPath,
|
|
342
697
|
testOutputDir,
|
|
343
698
|
debugOutputDir,
|
|
344
699
|
stdout: "",
|
|
345
|
-
stderr: `Failed to start maestro CLI: ${err.message}
|
|
700
|
+
stderr: `Failed to start maestro CLI: ${err.message}`,
|
|
701
|
+
networkJsonlDir
|
|
346
702
|
});
|
|
347
703
|
});
|
|
348
704
|
});
|
|
349
705
|
}
|
|
706
|
+
async function runMaestro(options, extras = {}) {
|
|
707
|
+
const tempDir = options.outputDir ? (0, import_node_path4.resolve)(options.outputDir) : generateTempDir();
|
|
708
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.join)(tempDir, "artifacts"), { recursive: true });
|
|
709
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.join)(tempDir, "debug"), { recursive: true });
|
|
710
|
+
const args = buildMaestroArgs(options, tempDir);
|
|
711
|
+
const junitPath = (0, import_node_path4.join)(tempDir, "report.xml");
|
|
712
|
+
const testOutputDir = (0, import_node_path4.join)(tempDir, "artifacts");
|
|
713
|
+
const debugOutputDir = (0, import_node_path4.join)(tempDir, "debug");
|
|
714
|
+
let proxyHandle = null;
|
|
715
|
+
let deviceHandle = null;
|
|
716
|
+
const networkConfig = extras.network;
|
|
717
|
+
if (networkConfig?.enabled) {
|
|
718
|
+
const networkDir = (0, import_node_path4.join)(tempDir, "network");
|
|
719
|
+
proxyHandle = await startNetworkProxy({
|
|
720
|
+
config: networkConfig,
|
|
721
|
+
outputDir: networkDir,
|
|
722
|
+
verbose: !options.quiet
|
|
723
|
+
});
|
|
724
|
+
if (proxyHandle) {
|
|
725
|
+
deviceHandle = await setUpDeviceProxy({
|
|
726
|
+
platform: normalisePlatform(options.platform),
|
|
727
|
+
deviceId: options.device,
|
|
728
|
+
proxyHost: proxyHandle.host,
|
|
729
|
+
proxyPort: proxyHandle.port,
|
|
730
|
+
skipCertInstall: networkConfig.skipCertInstall,
|
|
731
|
+
quiet: options.quiet
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const networkJsonlDir = proxyHandle?.outputDir ?? null;
|
|
736
|
+
const cleanup = async () => {
|
|
737
|
+
if (proxyHandle) {
|
|
738
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
739
|
+
}
|
|
740
|
+
if (deviceHandle) {
|
|
741
|
+
try {
|
|
742
|
+
await deviceHandle.teardown();
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (proxyHandle) {
|
|
747
|
+
try {
|
|
748
|
+
await proxyHandle.stop();
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
return runMaestroProc(args, junitPath, testOutputDir, debugOutputDir, networkJsonlDir, options.quiet, cleanup);
|
|
754
|
+
}
|
|
350
755
|
|
|
351
756
|
// src/report-orchestrator.ts
|
|
352
|
-
var
|
|
353
|
-
var
|
|
354
|
-
var
|
|
757
|
+
var import_node_fs18 = require("fs");
|
|
758
|
+
var import_node_path16 = require("path");
|
|
759
|
+
var import_node_crypto4 = require("crypto");
|
|
355
760
|
|
|
356
761
|
// src/parsers/junit-parser.ts
|
|
357
762
|
var import_fast_xml_parser = require("fast-xml-parser");
|
|
358
|
-
var
|
|
763
|
+
var import_node_fs5 = require("fs");
|
|
359
764
|
var parser = new import_fast_xml_parser.XMLParser({
|
|
360
765
|
ignoreAttributes: false,
|
|
361
766
|
attributeNamePrefix: "@_",
|
|
@@ -453,13 +858,13 @@ function parseJUnitXml(xmlContent) {
|
|
|
453
858
|
return { testSuites: suites, totalTests, totalFailures, totalErrors, totalSkipped, totalTime };
|
|
454
859
|
}
|
|
455
860
|
function parseJUnitFile(filePath) {
|
|
456
|
-
const content = (0,
|
|
861
|
+
const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
|
|
457
862
|
return parseJUnitXml(content);
|
|
458
863
|
}
|
|
459
864
|
|
|
460
865
|
// src/parsers/command-parser.ts
|
|
461
|
-
var
|
|
462
|
-
var
|
|
866
|
+
var import_node_fs6 = require("fs");
|
|
867
|
+
var import_node_path5 = require("path");
|
|
463
868
|
var INTERACTION_COMMANDS = /* @__PURE__ */ new Set([
|
|
464
869
|
"tapOn",
|
|
465
870
|
"doubleTapOn",
|
|
@@ -524,6 +929,9 @@ var AI_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
524
929
|
"assertNoDefectsWithAI",
|
|
525
930
|
"extractTextWithAI"
|
|
526
931
|
]);
|
|
932
|
+
var REDACTED_DETAIL_COMMANDS = /* @__PURE__ */ new Set(["inputText", "pasteText"]);
|
|
933
|
+
var MAX_SCRIPT_DETAIL_LENGTH = 200;
|
|
934
|
+
var MAX_AI_PROMPT_LENGTH = 400;
|
|
527
935
|
function categorizeCommand(command) {
|
|
528
936
|
if (AI_COMMANDS.has(command)) return "ai";
|
|
529
937
|
if (ASSERTION_COMMANDS.has(command)) return "assertion";
|
|
@@ -564,22 +972,165 @@ function extractMaestroSelector(params) {
|
|
|
564
972
|
if (typeof params.path === "string") return params.path;
|
|
565
973
|
return void 0;
|
|
566
974
|
}
|
|
975
|
+
function truncate(value, max) {
|
|
976
|
+
if (value.length <= max) return value;
|
|
977
|
+
return `${value.slice(0, max)}\u2026`;
|
|
978
|
+
}
|
|
979
|
+
function redactText(value) {
|
|
980
|
+
if (value.length === 0) return "";
|
|
981
|
+
return `[REDACTED ${value.length} chars]`;
|
|
982
|
+
}
|
|
983
|
+
function stringifyPoint(point) {
|
|
984
|
+
if (!point || typeof point !== "object") return void 0;
|
|
985
|
+
const p = point;
|
|
986
|
+
if (typeof p.x === "number" && typeof p.y === "number") return `${p.x},${p.y}`;
|
|
987
|
+
return void 0;
|
|
988
|
+
}
|
|
989
|
+
function extractCommandDetails(commandName, params) {
|
|
990
|
+
const details = {};
|
|
991
|
+
switch (commandName) {
|
|
992
|
+
case "inputText":
|
|
993
|
+
case "pasteText": {
|
|
994
|
+
const text = params.text ?? params.value;
|
|
995
|
+
if (typeof text === "string") {
|
|
996
|
+
details.text = REDACTED_DETAIL_COMMANDS.has(commandName) ? redactText(text) : text;
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
case "swipe":
|
|
1001
|
+
case "scroll":
|
|
1002
|
+
case "scrollUntilVisible": {
|
|
1003
|
+
if (typeof params.direction === "string") details.direction = params.direction;
|
|
1004
|
+
const startPoint = stringifyPoint(params.start ?? params.startRelative);
|
|
1005
|
+
const endPoint = stringifyPoint(params.end ?? params.endRelative);
|
|
1006
|
+
if (startPoint) details.start = startPoint;
|
|
1007
|
+
if (endPoint) details.end = endPoint;
|
|
1008
|
+
if (typeof params.duration === "number") details.swipeDurationMs = params.duration;
|
|
1009
|
+
if (typeof params.timeout === "number") details.timeoutMs = params.timeout;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
case "setLocation": {
|
|
1013
|
+
if (typeof params.latitude === "number") details.latitude = params.latitude;
|
|
1014
|
+
if (typeof params.longitude === "number") details.longitude = params.longitude;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
case "pressKey":
|
|
1018
|
+
case "back": {
|
|
1019
|
+
if (typeof params.key === "string") details.key = params.key;
|
|
1020
|
+
else if (typeof params.code === "string") details.key = params.code;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
case "setOrientation": {
|
|
1024
|
+
if (typeof params.orientation === "string") details.orientation = params.orientation;
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
case "setAirplaneMode":
|
|
1028
|
+
case "toggleAirplaneMode": {
|
|
1029
|
+
if (typeof params.value === "string" || typeof params.value === "boolean") {
|
|
1030
|
+
details.value = params.value;
|
|
1031
|
+
}
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
case "setPermissions": {
|
|
1035
|
+
if (params.permissions && typeof params.permissions === "object") {
|
|
1036
|
+
details.permissions = params.permissions;
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
case "addMedia": {
|
|
1041
|
+
if (Array.isArray(params.mediaPaths)) details.mediaPaths = params.mediaPaths;
|
|
1042
|
+
else if (typeof params.path === "string") details.mediaPaths = [params.path];
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
case "launchApp":
|
|
1046
|
+
case "killApp":
|
|
1047
|
+
case "stopApp":
|
|
1048
|
+
case "clearState": {
|
|
1049
|
+
const appId = params.appId ?? params.packageName ?? params.bundleId;
|
|
1050
|
+
if (typeof appId === "string") details.appId = appId;
|
|
1051
|
+
if (params.arguments) details.arguments = params.arguments;
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
case "openLink": {
|
|
1055
|
+
if (typeof params.url === "string") details.url = params.url;
|
|
1056
|
+
else if (typeof params.link === "string") details.url = params.link;
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
case "runScript":
|
|
1060
|
+
case "evalScript": {
|
|
1061
|
+
if (typeof params.path === "string") details.path = params.path;
|
|
1062
|
+
if (typeof params.script === "string") details.script = truncate(params.script, MAX_SCRIPT_DETAIL_LENGTH);
|
|
1063
|
+
if (typeof params.sourceDescription === "string") details.sourceDescription = params.sourceDescription;
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
case "assertWithAI":
|
|
1067
|
+
case "assertNoDefectsWithAI":
|
|
1068
|
+
case "extractTextWithAI": {
|
|
1069
|
+
const prompt = params.assertion ?? params.prompt ?? params.question;
|
|
1070
|
+
if (typeof prompt === "string") details.prompt = truncate(prompt, MAX_AI_PROMPT_LENGTH);
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case "extendedWaitUntil":
|
|
1074
|
+
case "waitForAnimationToEnd": {
|
|
1075
|
+
if (typeof params.timeout === "number") details.timeoutMs = params.timeout;
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
case "repeat":
|
|
1079
|
+
case "retry": {
|
|
1080
|
+
if (typeof params.times === "number") details.times = params.times;
|
|
1081
|
+
if (typeof params.maxRetries === "number") details.maxRetries = params.maxRetries;
|
|
1082
|
+
if (typeof params.condition === "object" && params.condition) details.condition = params.condition;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
case "runFlow": {
|
|
1086
|
+
if (typeof params.file === "string") details.file = params.file;
|
|
1087
|
+
if (typeof params.flowId === "string") details.flowId = params.flowId;
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
default:
|
|
1091
|
+
for (const key of ["text", "value", "url"]) {
|
|
1092
|
+
const v = params[key];
|
|
1093
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
1094
|
+
details[key] = v;
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return Object.keys(details).length > 0 ? details : void 0;
|
|
1100
|
+
}
|
|
1101
|
+
function extractChildCommands(params, raw, baseTimestamp) {
|
|
1102
|
+
const candidates = [params.commands, raw.commands, raw.subSteps, raw.children];
|
|
1103
|
+
for (const c of candidates) {
|
|
1104
|
+
if (Array.isArray(c) && c.length > 0) {
|
|
1105
|
+
return c.map(
|
|
1106
|
+
(entry, i) => parseRawCommand(entry, i, baseTimestamp)
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
567
1112
|
function parseRawCommand(raw, index, baseTimestamp) {
|
|
568
1113
|
let command;
|
|
569
1114
|
let selector;
|
|
1115
|
+
let details;
|
|
1116
|
+
let children = [];
|
|
570
1117
|
if (typeof raw.command === "object" && raw.command !== null && !Array.isArray(raw.command)) {
|
|
571
1118
|
const commandObj = raw.command;
|
|
572
1119
|
const rawKey = Object.keys(commandObj)[0] ?? `step-${index}`;
|
|
573
1120
|
command = normalizeMaestroCommandName(rawKey);
|
|
574
1121
|
const params = commandObj[rawKey] ?? {};
|
|
575
1122
|
selector = extractMaestroSelector(params);
|
|
1123
|
+
details = extractCommandDetails(command, params);
|
|
1124
|
+
children = extractChildCommands(params, raw, baseTimestamp);
|
|
576
1125
|
} else {
|
|
577
1126
|
command = raw.command ?? raw.commandName ?? raw.name ?? `step-${index}`;
|
|
578
1127
|
selector = raw.selector ?? void 0;
|
|
1128
|
+
children = extractChildCommands({}, raw, baseTimestamp);
|
|
579
1129
|
}
|
|
580
1130
|
const meta = raw.metadata ?? {};
|
|
581
1131
|
const status = mapStatus(meta.status ?? raw.status);
|
|
582
1132
|
const duration = meta.duration ?? raw.duration ?? raw.durationMs ?? 0;
|
|
1133
|
+
const sequenceNumber = typeof meta.sequenceNumber === "number" ? meta.sequenceNumber : void 0;
|
|
583
1134
|
let timestamp;
|
|
584
1135
|
if (typeof meta.timestamp === "number") {
|
|
585
1136
|
timestamp = new Date(meta.timestamp).toISOString();
|
|
@@ -594,7 +1145,10 @@ function parseRawCommand(raw, index, baseTimestamp) {
|
|
|
594
1145
|
duration,
|
|
595
1146
|
timestamp,
|
|
596
1147
|
...selector ? { selector } : {},
|
|
597
|
-
...error ? { error } : {}
|
|
1148
|
+
...error ? { error } : {},
|
|
1149
|
+
...details ? { details } : {},
|
|
1150
|
+
...sequenceNumber !== void 0 ? { sequenceNumber } : {},
|
|
1151
|
+
...children.length > 0 ? { children } : {}
|
|
598
1152
|
};
|
|
599
1153
|
}
|
|
600
1154
|
function mapStatus(status) {
|
|
@@ -625,24 +1179,24 @@ function parseCommandsJson(jsonContent, baseTimestamp) {
|
|
|
625
1179
|
}
|
|
626
1180
|
function parseCommandsFile(filePath, baseTimestamp) {
|
|
627
1181
|
try {
|
|
628
|
-
const content = (0,
|
|
1182
|
+
const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
|
|
629
1183
|
return parseCommandsJson(content, baseTimestamp);
|
|
630
1184
|
} catch {
|
|
631
1185
|
return [];
|
|
632
1186
|
}
|
|
633
1187
|
}
|
|
634
1188
|
function discoverCommandFiles(artifactsDir) {
|
|
635
|
-
if (!(0,
|
|
1189
|
+
if (!(0, import_node_fs6.existsSync)(artifactsDir)) return [];
|
|
636
1190
|
try {
|
|
637
|
-
return (0,
|
|
1191
|
+
return (0, import_node_fs6.readdirSync)(artifactsDir, { recursive: true }).map(String).filter((f) => (0, import_node_path5.basename)(f).startsWith("commands") && f.endsWith(".json")).map((f) => (0, import_node_path5.join)(artifactsDir, f));
|
|
638
1192
|
} catch {
|
|
639
1193
|
return [];
|
|
640
1194
|
}
|
|
641
1195
|
}
|
|
642
1196
|
|
|
643
1197
|
// src/parsers/flow-parser.ts
|
|
644
|
-
var
|
|
645
|
-
var
|
|
1198
|
+
var import_node_fs7 = require("fs");
|
|
1199
|
+
var import_node_path6 = require("path");
|
|
646
1200
|
var import_yaml = require("yaml");
|
|
647
1201
|
function extractSubflowRefs(body) {
|
|
648
1202
|
const refs = [];
|
|
@@ -736,21 +1290,21 @@ function parseFlowYaml(content, filePath) {
|
|
|
736
1290
|
};
|
|
737
1291
|
}
|
|
738
1292
|
function parseFlowFile(filePath) {
|
|
739
|
-
const content = (0,
|
|
1293
|
+
const content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
|
|
740
1294
|
return parseFlowYaml(content, filePath);
|
|
741
1295
|
}
|
|
742
1296
|
function discoverFlowFiles(dir) {
|
|
743
|
-
if (!(0,
|
|
1297
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
744
1298
|
const results = [];
|
|
745
1299
|
function walk(currentDir) {
|
|
746
1300
|
try {
|
|
747
|
-
const entries = (0,
|
|
1301
|
+
const entries = (0, import_node_fs7.readdirSync)(currentDir, { withFileTypes: true });
|
|
748
1302
|
for (const entry of entries) {
|
|
749
|
-
const fullPath = (0,
|
|
1303
|
+
const fullPath = (0, import_node_path6.join)(currentDir, entry.name);
|
|
750
1304
|
if (entry.isDirectory()) {
|
|
751
1305
|
walk(fullPath);
|
|
752
1306
|
} else if (entry.isFile()) {
|
|
753
|
-
const ext = (0,
|
|
1307
|
+
const ext = (0, import_node_path6.extname)(entry.name).toLowerCase();
|
|
754
1308
|
if (ext === ".yaml" || ext === ".yml") {
|
|
755
1309
|
results.push(fullPath);
|
|
756
1310
|
}
|
|
@@ -764,9 +1318,9 @@ function discoverFlowFiles(dir) {
|
|
|
764
1318
|
}
|
|
765
1319
|
|
|
766
1320
|
// src/parsers/log-parser.ts
|
|
767
|
-
var
|
|
768
|
-
var
|
|
769
|
-
var
|
|
1321
|
+
var import_node_fs8 = require("fs");
|
|
1322
|
+
var import_node_path7 = require("path");
|
|
1323
|
+
var import_node_fs9 = require("fs");
|
|
770
1324
|
var LOG_LINE_REGEX = /^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(\w+)\s+(.+)$/;
|
|
771
1325
|
var TIMESTAMP_ONLY_REGEX = /^\[?(\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]?\s+(\w+)\s+(.+)$/;
|
|
772
1326
|
function parseLevel(raw) {
|
|
@@ -816,7 +1370,7 @@ function parseLogContent(content) {
|
|
|
816
1370
|
}
|
|
817
1371
|
function parseLogFile(filePath) {
|
|
818
1372
|
try {
|
|
819
|
-
const content = (0,
|
|
1373
|
+
const content = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
|
|
820
1374
|
return parseLogContent(content);
|
|
821
1375
|
} catch {
|
|
822
1376
|
return [];
|
|
@@ -838,17 +1392,17 @@ function detectPlatformFromLogs(entries) {
|
|
|
838
1392
|
return "unknown";
|
|
839
1393
|
}
|
|
840
1394
|
function discoverLogFiles(dir) {
|
|
841
|
-
if (!(0,
|
|
1395
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
842
1396
|
try {
|
|
843
|
-
return (0,
|
|
1397
|
+
return (0, import_node_fs9.readdirSync)(dir, { recursive: true }).map(String).filter((f) => (0, import_node_path7.basename)(f) === "maestro.log" || f.endsWith(".log")).map((f) => (0, import_node_path7.join)(dir, f));
|
|
844
1398
|
} catch {
|
|
845
1399
|
return [];
|
|
846
1400
|
}
|
|
847
1401
|
}
|
|
848
1402
|
|
|
849
1403
|
// src/parsers/ai-report-parser.ts
|
|
850
|
-
var
|
|
851
|
-
var
|
|
1404
|
+
var import_node_fs10 = require("fs");
|
|
1405
|
+
var import_node_path8 = require("path");
|
|
852
1406
|
var DEFECT_SECTION_REGEX = /(?:defect|issue|bug|error|warning|problem)/i;
|
|
853
1407
|
var SEVERITY_CRITICAL_REGEX = /(?:critical|severe|major|blocker)/i;
|
|
854
1408
|
var SEVERITY_WARNING_REGEX = /(?:warning|moderate|minor)/i;
|
|
@@ -935,38 +1489,250 @@ function parseAiReportHtml(htmlContent) {
|
|
|
935
1489
|
}
|
|
936
1490
|
function parseAiReportFile(filePath) {
|
|
937
1491
|
try {
|
|
938
|
-
const content = (0,
|
|
1492
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
939
1493
|
return parseAiReportHtml(content);
|
|
940
1494
|
} catch {
|
|
941
1495
|
return { defects: [], totalDefects: 0, hasIssues: false, rawHtml: null };
|
|
942
1496
|
}
|
|
943
1497
|
}
|
|
944
1498
|
function discoverAiReports(dir) {
|
|
945
|
-
if (!(0,
|
|
1499
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
946
1500
|
try {
|
|
947
|
-
return (0,
|
|
948
|
-
const name = (0,
|
|
949
|
-
const ext = (0,
|
|
1501
|
+
return (0, import_node_fs10.readdirSync)(dir, { recursive: true }).map(String).filter((f) => {
|
|
1502
|
+
const name = (0, import_node_path8.basename)(f).toLowerCase();
|
|
1503
|
+
const ext = (0, import_node_path8.extname)(f).toLowerCase();
|
|
950
1504
|
return ext === ".html" && (name.includes("insight") || name.includes("ai") || name.includes("analysis"));
|
|
951
|
-
}).map((f) => (0,
|
|
1505
|
+
}).map((f) => (0, import_node_path8.join)(dir, f));
|
|
1506
|
+
} catch {
|
|
1507
|
+
return [];
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/parsers/network-parser.ts
|
|
1512
|
+
var import_node_fs11 = require("fs");
|
|
1513
|
+
var import_node_path9 = require("path");
|
|
1514
|
+
|
|
1515
|
+
// src/api-redactor.ts
|
|
1516
|
+
var REDACTED = "[REDACTED]";
|
|
1517
|
+
function redactHeaders(headers, redactList) {
|
|
1518
|
+
if (!headers) return null;
|
|
1519
|
+
const lower = new Set(redactList.map((h) => h.toLowerCase()));
|
|
1520
|
+
const out = {};
|
|
1521
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1522
|
+
out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
|
|
1523
|
+
}
|
|
1524
|
+
return out;
|
|
1525
|
+
}
|
|
1526
|
+
function redactJsonBody(value, fields) {
|
|
1527
|
+
if (value === null || typeof value !== "object") return value;
|
|
1528
|
+
if (Array.isArray(value)) return value.map((v) => redactJsonBody(v, fields));
|
|
1529
|
+
const out = {};
|
|
1530
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1531
|
+
out[k] = fields.has(k) ? REDACTED : redactJsonBody(v, fields);
|
|
1532
|
+
}
|
|
1533
|
+
return out;
|
|
1534
|
+
}
|
|
1535
|
+
function redactBody(body, fields) {
|
|
1536
|
+
if (!body || fields.length === 0) return body;
|
|
1537
|
+
try {
|
|
1538
|
+
const parsed = JSON.parse(body);
|
|
1539
|
+
const redacted = redactJsonBody(parsed, new Set(fields));
|
|
1540
|
+
return JSON.stringify(redacted);
|
|
1541
|
+
} catch {
|
|
1542
|
+
return body;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
function redactApiCall(call, redactHeadersList, redactBodyFields) {
|
|
1546
|
+
return {
|
|
1547
|
+
...call,
|
|
1548
|
+
requestHeaders: redactHeaders(call.requestHeaders, redactHeadersList),
|
|
1549
|
+
responseHeaders: redactHeaders(call.responseHeaders, redactHeadersList),
|
|
1550
|
+
requestBody: redactBody(call.requestBody, redactBodyFields),
|
|
1551
|
+
responseBody: call.isBinary ? call.responseBody : redactBody(call.responseBody, redactBodyFields)
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
function matchesUrlFilter(url, include, exclude) {
|
|
1555
|
+
const matches = (pat) => typeof pat === "string" ? url.includes(pat) : pat.test(url);
|
|
1556
|
+
if (exclude.some(matches)) return false;
|
|
1557
|
+
if (include.length === 0) return true;
|
|
1558
|
+
return include.some(matches);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/parsers/network-parser.ts
|
|
1562
|
+
function parseJsonlLine(line2) {
|
|
1563
|
+
const trimmed = line2.trim();
|
|
1564
|
+
if (!trimmed) return null;
|
|
1565
|
+
try {
|
|
1566
|
+
const obj = JSON.parse(trimmed);
|
|
1567
|
+
if (typeof obj.url !== "string" || typeof obj.method !== "string") return null;
|
|
1568
|
+
return {
|
|
1569
|
+
id: obj.id ?? `api-call-${Date.now()}`,
|
|
1570
|
+
timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1571
|
+
method: obj.method,
|
|
1572
|
+
url: obj.url,
|
|
1573
|
+
requestHeaders: obj.requestHeaders ?? null,
|
|
1574
|
+
requestBody: obj.requestBody ?? null,
|
|
1575
|
+
responseStatusCode: obj.responseStatusCode ?? null,
|
|
1576
|
+
responseStatusText: obj.responseStatusText ?? null,
|
|
1577
|
+
responseHeaders: obj.responseHeaders ?? null,
|
|
1578
|
+
responseBody: obj.responseBody ?? null,
|
|
1579
|
+
responseTimeMs: typeof obj.responseTimeMs === "number" ? obj.responseTimeMs : 0,
|
|
1580
|
+
isBinary: obj.isBinary ?? false,
|
|
1581
|
+
error: obj.error ?? null
|
|
1582
|
+
};
|
|
1583
|
+
} catch {
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function parseNetworkJsonl(content, config) {
|
|
1588
|
+
const records = [];
|
|
1589
|
+
for (const line2 of content.split(/\r?\n/)) {
|
|
1590
|
+
const rec = parseJsonlLine(line2);
|
|
1591
|
+
if (!rec) continue;
|
|
1592
|
+
if (!matchesUrlFilter(rec.url, config.includeUrls, config.excludeUrls)) continue;
|
|
1593
|
+
records.push(redactApiCall(rec, config.redactHeaders, config.redactBodyFields));
|
|
1594
|
+
}
|
|
1595
|
+
return records;
|
|
1596
|
+
}
|
|
1597
|
+
function parseNetworkJsonlFile(filePath, config) {
|
|
1598
|
+
try {
|
|
1599
|
+
return parseNetworkJsonl((0, import_node_fs11.readFileSync)(filePath, "utf-8"), config);
|
|
1600
|
+
} catch {
|
|
1601
|
+
return [];
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function discoverNetworkJsonlFiles(dir) {
|
|
1605
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return [];
|
|
1606
|
+
try {
|
|
1607
|
+
return (0, import_node_fs11.readdirSync)(dir, { recursive: true }).map(String).filter((f) => f.endsWith(".jsonl")).map((f) => (0, import_node_path9.join)(dir, f));
|
|
1608
|
+
} catch {
|
|
1609
|
+
return [];
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
function harHeadersToRecord(headers) {
|
|
1613
|
+
if (!headers || headers.length === 0) return null;
|
|
1614
|
+
const out = {};
|
|
1615
|
+
for (const h of headers) {
|
|
1616
|
+
if (typeof h.name === "string" && typeof h.value === "string") out[h.name] = h.value;
|
|
1617
|
+
}
|
|
1618
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
1619
|
+
}
|
|
1620
|
+
function harBodyToString(postData) {
|
|
1621
|
+
if (!postData) return null;
|
|
1622
|
+
if (typeof postData.text === "string") return postData.text;
|
|
1623
|
+
if (Array.isArray(postData.params)) {
|
|
1624
|
+
return postData.params.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value ?? "")}`).join("&");
|
|
1625
|
+
}
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
function isBinaryMime(mime) {
|
|
1629
|
+
if (!mime) return false;
|
|
1630
|
+
return /^(image|audio|video|application\/(octet-stream|pdf|zip|protobuf))/.test(mime);
|
|
1631
|
+
}
|
|
1632
|
+
function harEntryToRecord(entry, index) {
|
|
1633
|
+
const req = entry.request;
|
|
1634
|
+
const res = entry.response;
|
|
1635
|
+
if (!req?.method || !req.url) return null;
|
|
1636
|
+
const content = res?.content;
|
|
1637
|
+
const isBinary = isBinaryMime(content?.mimeType);
|
|
1638
|
+
return {
|
|
1639
|
+
id: `har-call-${index}`,
|
|
1640
|
+
timestamp: entry.startedDateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1641
|
+
method: req.method,
|
|
1642
|
+
url: req.url,
|
|
1643
|
+
requestHeaders: harHeadersToRecord(req.headers),
|
|
1644
|
+
requestBody: harBodyToString(req.postData),
|
|
1645
|
+
responseStatusCode: typeof res?.status === "number" && res.status > 0 ? res.status : null,
|
|
1646
|
+
responseStatusText: res?.statusText ?? null,
|
|
1647
|
+
responseHeaders: harHeadersToRecord(res?.headers),
|
|
1648
|
+
responseBody: content?.text ?? null,
|
|
1649
|
+
responseTimeMs: typeof entry.time === "number" ? entry.time : 0,
|
|
1650
|
+
isBinary,
|
|
1651
|
+
error: null
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function parseHar(content, config) {
|
|
1655
|
+
let parsed;
|
|
1656
|
+
try {
|
|
1657
|
+
parsed = JSON.parse(content);
|
|
1658
|
+
} catch {
|
|
1659
|
+
return [];
|
|
1660
|
+
}
|
|
1661
|
+
const entries = parsed.log?.entries ?? [];
|
|
1662
|
+
const records = [];
|
|
1663
|
+
entries.forEach((entry, i) => {
|
|
1664
|
+
const rec = harEntryToRecord(entry, i);
|
|
1665
|
+
if (!rec) return;
|
|
1666
|
+
if (!matchesUrlFilter(rec.url, config.includeUrls, config.excludeUrls)) return;
|
|
1667
|
+
records.push(redactApiCall(rec, config.redactHeaders, config.redactBodyFields));
|
|
1668
|
+
});
|
|
1669
|
+
return records;
|
|
1670
|
+
}
|
|
1671
|
+
function parseHarFile(filePath, config) {
|
|
1672
|
+
try {
|
|
1673
|
+
return parseHar((0, import_node_fs11.readFileSync)(filePath, "utf-8"), config);
|
|
952
1674
|
} catch {
|
|
953
1675
|
return [];
|
|
954
1676
|
}
|
|
955
1677
|
}
|
|
1678
|
+
function bindApiCallsToFlows(calls, flows) {
|
|
1679
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1680
|
+
for (const f of flows) buckets.set(f.testId, []);
|
|
1681
|
+
if (calls.length === 0 || flows.length === 0) return buckets;
|
|
1682
|
+
const windows = flows.map((f) => ({
|
|
1683
|
+
testId: f.testId,
|
|
1684
|
+
startMs: Date.parse(f.startedAt),
|
|
1685
|
+
endMs: Date.parse(f.completedAt)
|
|
1686
|
+
}));
|
|
1687
|
+
for (const call of calls) {
|
|
1688
|
+
const t = Date.parse(call.timestamp);
|
|
1689
|
+
if (!Number.isFinite(t)) continue;
|
|
1690
|
+
const hit = windows.find((w) => Number.isFinite(w.startMs) && Number.isFinite(w.endMs) && t >= w.startMs && t <= w.endMs);
|
|
1691
|
+
if (hit) {
|
|
1692
|
+
buckets.get(hit.testId).push(call);
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
let best = null;
|
|
1696
|
+
for (const w of windows) {
|
|
1697
|
+
if (!Number.isFinite(w.startMs) || !Number.isFinite(w.endMs)) continue;
|
|
1698
|
+
const dist = t < w.startMs ? w.startMs - t : t - w.endMs;
|
|
1699
|
+
if (!best || dist < best.dist) best = { testId: w.testId, dist };
|
|
1700
|
+
}
|
|
1701
|
+
if (best) buckets.get(best.testId).push(call);
|
|
1702
|
+
}
|
|
1703
|
+
return buckets;
|
|
1704
|
+
}
|
|
1705
|
+
function collectNetworkByFlow(jsonlDir, config, flows) {
|
|
1706
|
+
if (!config.enabled && !config.harPath) return /* @__PURE__ */ new Map();
|
|
1707
|
+
const all = [];
|
|
1708
|
+
if (jsonlDir) {
|
|
1709
|
+
for (const file of discoverNetworkJsonlFiles(jsonlDir)) {
|
|
1710
|
+
all.push(...parseNetworkJsonlFile(file, config));
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (config.harPath && (0, import_node_fs11.existsSync)(config.harPath)) {
|
|
1714
|
+
all.push(...parseHarFile(config.harPath, config));
|
|
1715
|
+
}
|
|
1716
|
+
if (all.length === 0) return /* @__PURE__ */ new Map();
|
|
1717
|
+
return bindApiCallsToFlows(all, flows);
|
|
1718
|
+
}
|
|
1719
|
+
function reindexFlowCalls(calls) {
|
|
1720
|
+
return calls.map((c, i) => ({ ...c, id: `api-call-${i}` }));
|
|
1721
|
+
}
|
|
956
1722
|
|
|
957
1723
|
// src/artifact-collector.ts
|
|
958
|
-
var
|
|
959
|
-
var
|
|
1724
|
+
var import_node_fs12 = require("fs");
|
|
1725
|
+
var import_node_path10 = require("path");
|
|
960
1726
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
961
1727
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov"]);
|
|
962
1728
|
function walkDir(dir) {
|
|
963
1729
|
const results = [];
|
|
964
|
-
if (!(0,
|
|
1730
|
+
if (!(0, import_node_fs12.existsSync)(dir)) return results;
|
|
965
1731
|
function recurse(current) {
|
|
966
1732
|
try {
|
|
967
|
-
const entries = (0,
|
|
1733
|
+
const entries = (0, import_node_fs12.readdirSync)(current, { withFileTypes: true });
|
|
968
1734
|
for (const entry of entries) {
|
|
969
|
-
const fullPath = (0,
|
|
1735
|
+
const fullPath = (0, import_node_path10.join)(current, entry.name);
|
|
970
1736
|
if (entry.isDirectory()) {
|
|
971
1737
|
recurse(fullPath);
|
|
972
1738
|
} else if (entry.isFile()) {
|
|
@@ -993,8 +1759,8 @@ function collectArtifacts(testOutputDir, debugOutputDir) {
|
|
|
993
1759
|
for (const filePath of allFiles) {
|
|
994
1760
|
if (seen.has(filePath)) continue;
|
|
995
1761
|
seen.add(filePath);
|
|
996
|
-
const name = (0,
|
|
997
|
-
const ext = (0,
|
|
1762
|
+
const name = (0, import_node_path10.basename)(filePath).toLowerCase();
|
|
1763
|
+
const ext = (0, import_node_path10.extname)(filePath).toLowerCase();
|
|
998
1764
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
999
1765
|
screenshotPaths.push(filePath);
|
|
1000
1766
|
continue;
|
|
@@ -1055,24 +1821,106 @@ function mapCommandCategory(category) {
|
|
|
1055
1821
|
return "custom_step";
|
|
1056
1822
|
}
|
|
1057
1823
|
}
|
|
1058
|
-
function
|
|
1824
|
+
function buildStepTitle(cmd) {
|
|
1825
|
+
const d = cmd.details;
|
|
1826
|
+
if (d) {
|
|
1827
|
+
switch (cmd.command) {
|
|
1828
|
+
case "inputText":
|
|
1829
|
+
case "pasteText":
|
|
1830
|
+
if (typeof d.text === "string") return `${cmd.command} \u2192 ${d.text}`;
|
|
1831
|
+
break;
|
|
1832
|
+
case "swipe":
|
|
1833
|
+
case "scroll":
|
|
1834
|
+
case "scrollUntilVisible":
|
|
1835
|
+
if (typeof d.direction === "string") return `${cmd.command} ${d.direction}`;
|
|
1836
|
+
break;
|
|
1837
|
+
case "setLocation":
|
|
1838
|
+
if (typeof d.latitude === "number" && typeof d.longitude === "number") {
|
|
1839
|
+
return `setLocation ${d.latitude},${d.longitude}`;
|
|
1840
|
+
}
|
|
1841
|
+
break;
|
|
1842
|
+
case "pressKey":
|
|
1843
|
+
if (typeof d.key === "string") return `pressKey ${d.key}`;
|
|
1844
|
+
break;
|
|
1845
|
+
case "setOrientation":
|
|
1846
|
+
if (typeof d.orientation === "string") return `setOrientation ${d.orientation}`;
|
|
1847
|
+
break;
|
|
1848
|
+
case "launchApp":
|
|
1849
|
+
case "killApp":
|
|
1850
|
+
case "stopApp":
|
|
1851
|
+
case "clearState":
|
|
1852
|
+
if (typeof d.appId === "string") return `${cmd.command} ${d.appId}`;
|
|
1853
|
+
break;
|
|
1854
|
+
case "openLink":
|
|
1855
|
+
if (typeof d.url === "string") return `openLink ${d.url}`;
|
|
1856
|
+
break;
|
|
1857
|
+
case "runScript":
|
|
1858
|
+
case "evalScript":
|
|
1859
|
+
if (typeof d.path === "string") return `${cmd.command} ${d.path}`;
|
|
1860
|
+
if (typeof d.sourceDescription === "string") return `${cmd.command} ${d.sourceDescription}`;
|
|
1861
|
+
break;
|
|
1862
|
+
case "assertWithAI":
|
|
1863
|
+
case "assertNoDefectsWithAI":
|
|
1864
|
+
case "extractTextWithAI":
|
|
1865
|
+
if (typeof d.prompt === "string") return `${cmd.command} \u2192 ${d.prompt}`;
|
|
1866
|
+
break;
|
|
1867
|
+
case "retry":
|
|
1868
|
+
if (typeof d.maxRetries === "number") return `retry \xD7 ${d.maxRetries}`;
|
|
1869
|
+
break;
|
|
1870
|
+
case "repeat":
|
|
1871
|
+
if (typeof d.times === "number") return `repeat \xD7 ${d.times}`;
|
|
1872
|
+
break;
|
|
1873
|
+
case "runFlow":
|
|
1874
|
+
if (typeof d.file === "string") return `runFlow ${d.file}`;
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return cmd.selector ? `${cmd.command} \u2192 ${cmd.selector}` : cmd.command;
|
|
1879
|
+
}
|
|
1880
|
+
function commandToAction(cmd, flowStartedMs) {
|
|
1881
|
+
const cmdMs = Date.parse(cmd.timestamp);
|
|
1882
|
+
const videoOffset = Number.isFinite(cmdMs) && Number.isFinite(flowStartedMs) ? Math.max(0, (cmdMs - flowStartedMs) / 1e3) : null;
|
|
1883
|
+
const children = (cmd.children ?? []).map((c) => commandToAction(c, flowStartedMs));
|
|
1059
1884
|
return {
|
|
1060
|
-
title: cmd
|
|
1885
|
+
title: buildStepTitle(cmd),
|
|
1061
1886
|
category: mapCommandCategory(cmd.category),
|
|
1062
1887
|
status: cmd.status === "failed" ? "failed" : "passed",
|
|
1063
1888
|
duration: cmd.duration,
|
|
1064
1889
|
timestamp: cmd.timestamp,
|
|
1065
|
-
videoOffset
|
|
1890
|
+
videoOffset,
|
|
1066
1891
|
error: cmd.error ?? null,
|
|
1067
|
-
children
|
|
1892
|
+
children
|
|
1068
1893
|
};
|
|
1069
1894
|
}
|
|
1070
|
-
function
|
|
1895
|
+
function sortActions(a, b) {
|
|
1896
|
+
const ta = Date.parse(a.timestamp);
|
|
1897
|
+
const tb = Date.parse(b.timestamp);
|
|
1898
|
+
if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
|
|
1899
|
+
return 0;
|
|
1900
|
+
}
|
|
1901
|
+
function buildTestResult(flow, apiCalls) {
|
|
1902
|
+
const flowStartedMs = Date.parse(flow.startedAt);
|
|
1903
|
+
const orderedCommands = [...flow.commands].sort((a, b) => {
|
|
1904
|
+
const ta = Date.parse(a.timestamp);
|
|
1905
|
+
const tb = Date.parse(b.timestamp);
|
|
1906
|
+
if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
|
|
1907
|
+
const sa = a.sequenceNumber ?? 0;
|
|
1908
|
+
const sb = b.sequenceNumber ?? 0;
|
|
1909
|
+
return sa - sb;
|
|
1910
|
+
});
|
|
1911
|
+
const orderedAssertions = [...flow.assertions].sort((a, b) => {
|
|
1912
|
+
const ta = Date.parse(a.timestamp);
|
|
1913
|
+
const tb = Date.parse(b.timestamp);
|
|
1914
|
+
if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
|
|
1915
|
+
const sa = a.sequenceNumber ?? 0;
|
|
1916
|
+
const sb = b.sequenceNumber ?? 0;
|
|
1917
|
+
return sa - sb;
|
|
1918
|
+
});
|
|
1071
1919
|
const actions = [
|
|
1072
|
-
...
|
|
1073
|
-
...
|
|
1920
|
+
...orderedCommands.map((c) => commandToAction(c, flowStartedMs)),
|
|
1921
|
+
...orderedAssertions.map((c) => commandToAction(c, flowStartedMs))
|
|
1074
1922
|
];
|
|
1075
|
-
actions.sort(
|
|
1923
|
+
actions.sort(sortActions);
|
|
1076
1924
|
const artifacts = flow.screenshotPaths.length > 0 || flow.videoPath ? {
|
|
1077
1925
|
screenshot: flow.screenshotPaths[0] ?? void 0,
|
|
1078
1926
|
video: flow.videoPath ?? void 0
|
|
@@ -1100,14 +1948,16 @@ function buildTestResult(flow) {
|
|
|
1100
1948
|
actualStatus: flow.status,
|
|
1101
1949
|
artifacts,
|
|
1102
1950
|
networkRequests: null,
|
|
1103
|
-
apiCalls: null,
|
|
1951
|
+
apiCalls: apiCalls.length > 0 ? [...apiCalls] : null,
|
|
1104
1952
|
apiAssertions: null,
|
|
1105
1953
|
actions: actions.length > 0 ? actions : null,
|
|
1106
1954
|
consoleLogs: null
|
|
1107
1955
|
};
|
|
1108
1956
|
}
|
|
1109
|
-
function buildTimelineEntry(flow) {
|
|
1110
|
-
const
|
|
1957
|
+
function buildTimelineEntry(flow, apiCallsByFlow) {
|
|
1958
|
+
const testId = `${flow.flowFile}::${flow.flowName}`;
|
|
1959
|
+
const apiCalls = apiCallsByFlow?.get(testId) ?? [];
|
|
1960
|
+
const testResult = buildTestResult(flow, apiCalls);
|
|
1111
1961
|
return {
|
|
1112
1962
|
url: flow.appId ?? "maestro-flow",
|
|
1113
1963
|
navigationType: "page_load",
|
|
@@ -1120,8 +1970,8 @@ function buildTimelineEntry(flow) {
|
|
|
1120
1970
|
tests: [testResult]
|
|
1121
1971
|
};
|
|
1122
1972
|
}
|
|
1123
|
-
function buildTimeline(flows) {
|
|
1124
|
-
return flows.map(buildTimelineEntry);
|
|
1973
|
+
function buildTimeline(flows, apiCallsByFlow) {
|
|
1974
|
+
return flows.map((f) => buildTimelineEntry(f, apiCallsByFlow));
|
|
1125
1975
|
}
|
|
1126
1976
|
|
|
1127
1977
|
// src/summary-builder.ts
|
|
@@ -1132,6 +1982,58 @@ var EMPTY_STATUS_RANGE = {
|
|
|
1132
1982
|
"5xx": 0,
|
|
1133
1983
|
error: 0
|
|
1134
1984
|
};
|
|
1985
|
+
function statusBucket(code) {
|
|
1986
|
+
if (code === null) return "error";
|
|
1987
|
+
if (code >= 200 && code < 300) return "2xx";
|
|
1988
|
+
if (code >= 300 && code < 400) return "3xx";
|
|
1989
|
+
if (code >= 400 && code < 500) return "4xx";
|
|
1990
|
+
if (code >= 500 && code < 600) return "5xx";
|
|
1991
|
+
return "error";
|
|
1992
|
+
}
|
|
1993
|
+
function percentile(sorted, p) {
|
|
1994
|
+
if (sorted.length === 0) return null;
|
|
1995
|
+
const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
|
|
1996
|
+
return sorted[idx];
|
|
1997
|
+
}
|
|
1998
|
+
function buildApiStats(calls) {
|
|
1999
|
+
if (calls.length === 0) {
|
|
2000
|
+
return {
|
|
2001
|
+
totalApiCalls: 0,
|
|
2002
|
+
uniqueApiUrls: 0,
|
|
2003
|
+
apiCallsByMethod: {},
|
|
2004
|
+
apiCallsByStatusRange: { ...EMPTY_STATUS_RANGE },
|
|
2005
|
+
apiResponseTime: null
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
const byMethod = {};
|
|
2009
|
+
const byStatus = { ...EMPTY_STATUS_RANGE };
|
|
2010
|
+
const urls = /* @__PURE__ */ new Set();
|
|
2011
|
+
const times = [];
|
|
2012
|
+
for (const c of calls) {
|
|
2013
|
+
byMethod[c.method] = (byMethod[c.method] ?? 0) + 1;
|
|
2014
|
+
byStatus[statusBucket(c.responseStatusCode)] += 1;
|
|
2015
|
+
urls.add(c.url);
|
|
2016
|
+
if (Number.isFinite(c.responseTimeMs) && c.responseTimeMs > 0) {
|
|
2017
|
+
times.push(c.responseTimeMs);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
times.sort((a, b) => a - b);
|
|
2021
|
+
const apiResponseTime = times.length > 0 ? {
|
|
2022
|
+
p50: percentile(times, 50) ?? 0,
|
|
2023
|
+
p95: percentile(times, 95) ?? 0,
|
|
2024
|
+
p99: percentile(times, 99) ?? 0,
|
|
2025
|
+
avg: times.reduce((s, n) => s + n, 0) / times.length,
|
|
2026
|
+
min: times[0],
|
|
2027
|
+
max: times[times.length - 1]
|
|
2028
|
+
} : null;
|
|
2029
|
+
return {
|
|
2030
|
+
totalApiCalls: calls.length,
|
|
2031
|
+
uniqueApiUrls: urls.size,
|
|
2032
|
+
apiCallsByMethod: byMethod,
|
|
2033
|
+
apiCallsByStatusRange: byStatus,
|
|
2034
|
+
apiResponseTime
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
1135
2037
|
function buildSummary(timeline) {
|
|
1136
2038
|
let total = 0;
|
|
1137
2039
|
let passed = 0;
|
|
@@ -1146,6 +2048,7 @@ function buildSummary(timeline) {
|
|
|
1146
2048
|
let totalActionSteps = 0;
|
|
1147
2049
|
const actionCategoryCounts = {};
|
|
1148
2050
|
const uniqueUrls = /* @__PURE__ */ new Set();
|
|
2051
|
+
const allApiCalls = [];
|
|
1149
2052
|
for (const entry of timeline) {
|
|
1150
2053
|
uniqueUrls.add(entry.url);
|
|
1151
2054
|
for (const test of entry.tests) {
|
|
@@ -1180,8 +2083,12 @@ function buildSummary(timeline) {
|
|
|
1180
2083
|
}
|
|
1181
2084
|
}
|
|
1182
2085
|
}
|
|
2086
|
+
if (test.apiCalls && test.apiCalls.length > 0) {
|
|
2087
|
+
allApiCalls.push(...test.apiCalls);
|
|
2088
|
+
}
|
|
1183
2089
|
}
|
|
1184
2090
|
}
|
|
2091
|
+
const apiStats = buildApiStats(allApiCalls);
|
|
1185
2092
|
return {
|
|
1186
2093
|
total,
|
|
1187
2094
|
passed,
|
|
@@ -1189,11 +2096,7 @@ function buildSummary(timeline) {
|
|
|
1189
2096
|
flaky,
|
|
1190
2097
|
skipped,
|
|
1191
2098
|
timedout,
|
|
1192
|
-
|
|
1193
|
-
uniqueApiUrls: 0,
|
|
1194
|
-
apiCallsByMethod: {},
|
|
1195
|
-
apiCallsByStatusRange: EMPTY_STATUS_RANGE,
|
|
1196
|
-
apiResponseTime: null,
|
|
2099
|
+
...apiStats,
|
|
1197
2100
|
totalAssertions,
|
|
1198
2101
|
passedAssertions,
|
|
1199
2102
|
failedAssertions,
|
|
@@ -1206,8 +2109,8 @@ function buildSummary(timeline) {
|
|
|
1206
2109
|
}
|
|
1207
2110
|
|
|
1208
2111
|
// src/html-report.ts
|
|
1209
|
-
var
|
|
1210
|
-
var
|
|
2112
|
+
var import_node_fs13 = require("fs");
|
|
2113
|
+
var import_node_path11 = require("path");
|
|
1211
2114
|
|
|
1212
2115
|
// src/html-css.ts
|
|
1213
2116
|
var CSS = `
|
|
@@ -2469,13 +3372,13 @@ function renderHtmlDocument(reportJson) {
|
|
|
2469
3372
|
|
|
2470
3373
|
// src/html-report.ts
|
|
2471
3374
|
function generateHtmlReport(report, aiDefects, screenshotPaths, outputPath) {
|
|
2472
|
-
const htmlPath = (0,
|
|
2473
|
-
const htmlDir = (0,
|
|
2474
|
-
if (!(0,
|
|
3375
|
+
const htmlPath = (0, import_node_path11.resolve)(outputPath);
|
|
3376
|
+
const htmlDir = (0, import_node_path11.dirname)(htmlPath);
|
|
3377
|
+
if (!(0, import_node_fs13.existsSync)(htmlDir)) (0, import_node_fs13.mkdirSync)(htmlDir, { recursive: true });
|
|
2475
3378
|
const payload = { report, aiDefects, screenshotPaths };
|
|
2476
3379
|
const reportJson = JSON.stringify(payload);
|
|
2477
3380
|
const html = renderHtmlDocument(reportJson);
|
|
2478
|
-
(0,
|
|
3381
|
+
(0, import_node_fs13.writeFileSync)(htmlPath, html, "utf-8");
|
|
2479
3382
|
}
|
|
2480
3383
|
|
|
2481
3384
|
// src/console-summary.ts
|
|
@@ -2538,7 +3441,7 @@ function printConsoleSummary(summary, outputPath, htmlReportPath, quiet, aiDefec
|
|
|
2538
3441
|
}
|
|
2539
3442
|
|
|
2540
3443
|
// src/browser-open.ts
|
|
2541
|
-
var
|
|
3444
|
+
var import_node_child_process4 = require("child_process");
|
|
2542
3445
|
function openInBrowser(filePath) {
|
|
2543
3446
|
const platform = process.platform;
|
|
2544
3447
|
let command;
|
|
@@ -2549,14 +3452,14 @@ function openInBrowser(filePath) {
|
|
|
2549
3452
|
} else {
|
|
2550
3453
|
command = `xdg-open "${filePath}"`;
|
|
2551
3454
|
}
|
|
2552
|
-
(0,
|
|
3455
|
+
(0, import_node_child_process4.exec)(command, () => {
|
|
2553
3456
|
});
|
|
2554
3457
|
}
|
|
2555
3458
|
|
|
2556
3459
|
// src/cloud-client.ts
|
|
2557
|
-
var
|
|
2558
|
-
var
|
|
2559
|
-
var
|
|
3460
|
+
var import_node_fs16 = require("fs");
|
|
3461
|
+
var import_node_path14 = require("path");
|
|
3462
|
+
var import_node_crypto3 = require("crypto");
|
|
2560
3463
|
|
|
2561
3464
|
// src/cloud-auth.ts
|
|
2562
3465
|
var import_core3 = require("@testrelic/core");
|
|
@@ -2587,7 +3490,7 @@ async function healthCheck(endpoint) {
|
|
|
2587
3490
|
}
|
|
2588
3491
|
}
|
|
2589
3492
|
async function sleep(ms) {
|
|
2590
|
-
return new Promise((
|
|
3493
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
2591
3494
|
}
|
|
2592
3495
|
async function parseCloudError(response) {
|
|
2593
3496
|
try {
|
|
@@ -2737,13 +3640,13 @@ async function resolveRepo(endpoint, accessToken, gitId, displayName, timeout, b
|
|
|
2737
3640
|
}
|
|
2738
3641
|
|
|
2739
3642
|
// src/git-metadata.ts
|
|
2740
|
-
var
|
|
2741
|
-
var
|
|
2742
|
-
var
|
|
3643
|
+
var import_node_child_process5 = require("child_process");
|
|
3644
|
+
var import_node_fs14 = require("fs");
|
|
3645
|
+
var import_node_path12 = require("path");
|
|
2743
3646
|
var GIT_TIMEOUT_MS = 5e3;
|
|
2744
3647
|
function execGit(command, cwd) {
|
|
2745
3648
|
try {
|
|
2746
|
-
const result = (0,
|
|
3649
|
+
const result = (0, import_node_child_process5.execSync)(command, {
|
|
2747
3650
|
cwd,
|
|
2748
3651
|
timeout: GIT_TIMEOUT_MS,
|
|
2749
3652
|
encoding: "utf-8",
|
|
@@ -2788,7 +3691,7 @@ function deriveRepoDisplayName(normalizedUrl) {
|
|
|
2788
3691
|
}
|
|
2789
3692
|
function readPackageJsonName(dirPath) {
|
|
2790
3693
|
try {
|
|
2791
|
-
const raw = (0,
|
|
3694
|
+
const raw = (0, import_node_fs14.readFileSync)((0, import_node_path12.join)(dirPath, "package.json"), "utf-8");
|
|
2792
3695
|
const pkg = JSON.parse(raw);
|
|
2793
3696
|
return typeof pkg.name === "string" && pkg.name.length > 0 ? pkg.name : null;
|
|
2794
3697
|
} catch {
|
|
@@ -2798,25 +3701,25 @@ function readPackageJsonName(dirPath) {
|
|
|
2798
3701
|
function deriveNonGitProjectId(dirPath) {
|
|
2799
3702
|
const pkgName = readPackageJsonName(dirPath);
|
|
2800
3703
|
if (pkgName) return pkgName;
|
|
2801
|
-
return `local/${(0,
|
|
3704
|
+
return `local/${(0, import_node_path12.basename)(dirPath)}`;
|
|
2802
3705
|
}
|
|
2803
3706
|
|
|
2804
3707
|
// src/cloud-queue.ts
|
|
2805
|
-
var
|
|
2806
|
-
var
|
|
3708
|
+
var import_node_fs15 = require("fs");
|
|
3709
|
+
var import_node_path13 = require("path");
|
|
2807
3710
|
var import_core4 = require("@testrelic/core");
|
|
2808
3711
|
var QUEUE_ENTRY_VERSION = "1.0.0";
|
|
2809
3712
|
var FLUSH_BATCH_SIZE = 10;
|
|
2810
3713
|
function writeToQueue(queueDirectory, runId, type, reason, targetEndpoint, method, payload, headers) {
|
|
2811
3714
|
try {
|
|
2812
|
-
(0,
|
|
3715
|
+
(0, import_node_fs15.mkdirSync)(queueDirectory, { recursive: true });
|
|
2813
3716
|
const timestamp = Date.now();
|
|
2814
3717
|
const filename = `${timestamp}-${runId}-${type}.json`;
|
|
2815
|
-
const filePath = (0,
|
|
3718
|
+
const filePath = (0, import_node_path13.join)(queueDirectory, filename);
|
|
2816
3719
|
const entry = { version: QUEUE_ENTRY_VERSION, queuedAt: new Date(timestamp).toISOString(), reason, retryCount: 0, targetEndpoint, method, payload, headers };
|
|
2817
3720
|
const tmpPath = filePath + ".tmp";
|
|
2818
|
-
(0,
|
|
2819
|
-
(0,
|
|
3721
|
+
(0, import_node_fs15.writeFileSync)(tmpPath, JSON.stringify(entry, null, 2), "utf-8");
|
|
3722
|
+
(0, import_node_fs15.renameSync)(tmpPath, filePath);
|
|
2820
3723
|
} catch (err) {
|
|
2821
3724
|
const code = err.code;
|
|
2822
3725
|
if (code === "ENOSPC") {
|
|
@@ -2829,7 +3732,7 @@ function writeToQueue(queueDirectory, runId, type, reason, targetEndpoint, metho
|
|
|
2829
3732
|
async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
2830
3733
|
let files;
|
|
2831
3734
|
try {
|
|
2832
|
-
files = (0,
|
|
3735
|
+
files = (0, import_node_fs15.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp")).sort();
|
|
2833
3736
|
} catch {
|
|
2834
3737
|
return;
|
|
2835
3738
|
}
|
|
@@ -2840,14 +3743,14 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
|
2840
3743
|
}
|
|
2841
3744
|
for (const batch of batches) {
|
|
2842
3745
|
for (const file of batch) {
|
|
2843
|
-
const filePath = (0,
|
|
3746
|
+
const filePath = (0, import_node_path13.join)(queueDirectory, file);
|
|
2844
3747
|
try {
|
|
2845
|
-
const stat = (0,
|
|
3748
|
+
const stat = (0, import_node_fs15.statSync)(filePath);
|
|
2846
3749
|
if (!stat.isFile()) continue;
|
|
2847
|
-
const raw = (0,
|
|
3750
|
+
const raw = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
|
|
2848
3751
|
const entry = JSON.parse(raw);
|
|
2849
3752
|
if (!(0, import_core4.isValidQueueEntry)(entry)) {
|
|
2850
|
-
(0,
|
|
3753
|
+
(0, import_node_fs15.unlinkSync)(filePath);
|
|
2851
3754
|
continue;
|
|
2852
3755
|
}
|
|
2853
3756
|
const queueEntry = entry;
|
|
@@ -2857,7 +3760,7 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
|
2857
3760
|
body: JSON.stringify(queueEntry.payload)
|
|
2858
3761
|
});
|
|
2859
3762
|
if (response.ok) {
|
|
2860
|
-
(0,
|
|
3763
|
+
(0, import_node_fs15.unlinkSync)(filePath);
|
|
2861
3764
|
} else {
|
|
2862
3765
|
return;
|
|
2863
3766
|
}
|
|
@@ -2869,21 +3772,21 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
|
2869
3772
|
}
|
|
2870
3773
|
function cleanupExpiredQueue(queueDirectory, maxAge) {
|
|
2871
3774
|
try {
|
|
2872
|
-
const files = (0,
|
|
3775
|
+
const files = (0, import_node_fs15.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
|
|
2873
3776
|
const now = Date.now();
|
|
2874
3777
|
for (const file of files) {
|
|
2875
|
-
const filePath = (0,
|
|
3778
|
+
const filePath = (0, import_node_path13.join)(queueDirectory, file);
|
|
2876
3779
|
try {
|
|
2877
|
-
const raw = (0,
|
|
3780
|
+
const raw = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
|
|
2878
3781
|
const entry = JSON.parse(raw);
|
|
2879
3782
|
const queuedAt = entry.queuedAt;
|
|
2880
3783
|
if (typeof queuedAt === "string") {
|
|
2881
3784
|
const queuedTime = new Date(queuedAt).getTime();
|
|
2882
|
-
if (now - queuedTime > maxAge) (0,
|
|
3785
|
+
if (now - queuedTime > maxAge) (0, import_node_fs15.unlinkSync)(filePath);
|
|
2883
3786
|
}
|
|
2884
3787
|
} catch {
|
|
2885
3788
|
try {
|
|
2886
|
-
(0,
|
|
3789
|
+
(0, import_node_fs15.unlinkSync)(filePath);
|
|
2887
3790
|
} catch {
|
|
2888
3791
|
}
|
|
2889
3792
|
}
|
|
@@ -3026,7 +3929,7 @@ var CloudClient = class {
|
|
|
3026
3929
|
}
|
|
3027
3930
|
if (this.flushPromise) {
|
|
3028
3931
|
try {
|
|
3029
|
-
await Promise.race([this.flushPromise, new Promise((
|
|
3932
|
+
await Promise.race([this.flushPromise, new Promise((resolve6) => setTimeout(resolve6, FLUSH_TIMEOUT_MS))]);
|
|
3030
3933
|
} catch {
|
|
3031
3934
|
}
|
|
3032
3935
|
this.flushPromise = null;
|
|
@@ -3084,10 +3987,10 @@ var CloudClient = class {
|
|
|
3084
3987
|
}
|
|
3085
3988
|
readRepoCache(gitId) {
|
|
3086
3989
|
if (!this.config) return null;
|
|
3087
|
-
const cachePath = (0,
|
|
3990
|
+
const cachePath = (0, import_node_path14.join)(this.config.queueDirectory, "..", "cache", "repo.json");
|
|
3088
3991
|
try {
|
|
3089
|
-
if (!(0,
|
|
3090
|
-
const raw = (0,
|
|
3992
|
+
if (!(0, import_node_fs16.existsSync)(cachePath)) return null;
|
|
3993
|
+
const raw = (0, import_node_fs16.readFileSync)(cachePath, "utf-8");
|
|
3091
3994
|
const cache = JSON.parse(raw);
|
|
3092
3995
|
if (cache.gitId !== gitId) return null;
|
|
3093
3996
|
if (Date.now() - cache.resolvedAt > PROJECT_CACHE_TTL_MS) return null;
|
|
@@ -3100,20 +4003,20 @@ var CloudClient = class {
|
|
|
3100
4003
|
}
|
|
3101
4004
|
writeRepoCache(gitId, repoId, displayName) {
|
|
3102
4005
|
if (!this.config) return;
|
|
3103
|
-
const cacheDir = (0,
|
|
3104
|
-
const cachePath = (0,
|
|
4006
|
+
const cacheDir = (0, import_node_path14.join)(this.config.queueDirectory, "..", "cache");
|
|
4007
|
+
const cachePath = (0, import_node_path14.join)(cacheDir, "repo.json");
|
|
3105
4008
|
try {
|
|
3106
|
-
(0,
|
|
4009
|
+
(0, import_node_fs16.mkdirSync)(cacheDir, { recursive: true });
|
|
3107
4010
|
const cache = { repoId, gitId, displayName, resolvedAt: Date.now(), apiKeyHash: this.hashApiKey() ?? "" };
|
|
3108
4011
|
const tmpPath = cachePath + ".tmp";
|
|
3109
|
-
(0,
|
|
3110
|
-
(0,
|
|
4012
|
+
(0, import_node_fs16.writeFileSync)(tmpPath, JSON.stringify(cache, null, 2), "utf-8");
|
|
4013
|
+
(0, import_node_fs16.renameSync)(tmpPath, cachePath);
|
|
3111
4014
|
} catch {
|
|
3112
4015
|
}
|
|
3113
4016
|
}
|
|
3114
4017
|
hashApiKey() {
|
|
3115
4018
|
if (!this.config?.apiKey) return null;
|
|
3116
|
-
return (0,
|
|
4019
|
+
return (0, import_node_crypto3.createHash)("sha256").update(this.config.apiKey).digest("hex").substring(0, 16);
|
|
3117
4020
|
}
|
|
3118
4021
|
startBackgroundFlush() {
|
|
3119
4022
|
if (!this.config || !this.authState.accessToken) return;
|
|
@@ -3122,7 +4025,7 @@ var CloudClient = class {
|
|
|
3122
4025
|
const endpoint = this.config.endpoint;
|
|
3123
4026
|
this.flushPromise = Promise.race([
|
|
3124
4027
|
flushQueue(queueDir, endpoint, accessToken),
|
|
3125
|
-
new Promise((
|
|
4028
|
+
new Promise((resolve6) => setTimeout(resolve6, FLUSH_TIMEOUT_MS))
|
|
3126
4029
|
]).catch(() => {
|
|
3127
4030
|
});
|
|
3128
4031
|
}
|
|
@@ -3179,7 +4082,7 @@ function buildUploadPayload(report, repoGitId, git, ci, tests) {
|
|
|
3179
4082
|
return payload;
|
|
3180
4083
|
}
|
|
3181
4084
|
async function sleep2(ms) {
|
|
3182
|
-
return new Promise((
|
|
4085
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
3183
4086
|
}
|
|
3184
4087
|
async function retryWithBackoff(url, init, onTokenRefresh) {
|
|
3185
4088
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
@@ -3255,8 +4158,8 @@ async function uploadBatchRun(endpoint, accessToken, payload, onTokenRefresh) {
|
|
|
3255
4158
|
}
|
|
3256
4159
|
|
|
3257
4160
|
// src/cloud-artifact-upload.ts
|
|
3258
|
-
var
|
|
3259
|
-
var
|
|
4161
|
+
var import_node_fs17 = require("fs");
|
|
4162
|
+
var import_node_path15 = require("path");
|
|
3260
4163
|
var import_node_stream = require("stream");
|
|
3261
4164
|
var UPLOAD_CONCURRENCY = 5;
|
|
3262
4165
|
var RETRY_DELAYS_MS2 = [1e3, 3e3, 9e3];
|
|
@@ -3275,7 +4178,7 @@ var activeUploads = 0;
|
|
|
3275
4178
|
var pendingUploads = [];
|
|
3276
4179
|
async function acquireSlot() {
|
|
3277
4180
|
if (activeUploads >= UPLOAD_CONCURRENCY) {
|
|
3278
|
-
await new Promise((
|
|
4181
|
+
await new Promise((resolve6) => pendingUploads.push(resolve6));
|
|
3279
4182
|
}
|
|
3280
4183
|
activeUploads++;
|
|
3281
4184
|
}
|
|
@@ -3284,7 +4187,7 @@ function releaseSlot() {
|
|
|
3284
4187
|
if (pendingUploads.length > 0) pendingUploads.shift()();
|
|
3285
4188
|
}
|
|
3286
4189
|
async function sleep3(ms) {
|
|
3287
|
-
return new Promise((
|
|
4190
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
3288
4191
|
}
|
|
3289
4192
|
function getContentType(filePath) {
|
|
3290
4193
|
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
@@ -3292,14 +4195,14 @@ function getContentType(filePath) {
|
|
|
3292
4195
|
}
|
|
3293
4196
|
function getFileSize(filePath) {
|
|
3294
4197
|
try {
|
|
3295
|
-
return (0,
|
|
4198
|
+
return (0, import_node_fs17.statSync)(filePath).size;
|
|
3296
4199
|
} catch {
|
|
3297
4200
|
return 0;
|
|
3298
4201
|
}
|
|
3299
4202
|
}
|
|
3300
4203
|
async function requestUploadUrl(endpoint, accessToken, request, sizeBytes, contentType) {
|
|
3301
4204
|
const url = `${endpoint}/artifacts/upload-url`;
|
|
3302
|
-
const body = JSON.stringify({ runId: request.runId, testId: request.testId, fileName: (0,
|
|
4205
|
+
const body = JSON.stringify({ runId: request.runId, testId: request.testId, fileName: (0, import_node_path15.basename)(request.filePath), contentType, type: request.type, sizeBytes });
|
|
3303
4206
|
for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
|
|
3304
4207
|
try {
|
|
3305
4208
|
const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${accessToken}` }, body });
|
|
@@ -3322,7 +4225,7 @@ async function requestUploadUrl(endpoint, accessToken, request, sizeBytes, conte
|
|
|
3322
4225
|
async function putFileToPresignedUrl(presignedUrl, filePath, contentType, sizeBytes) {
|
|
3323
4226
|
for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
|
|
3324
4227
|
try {
|
|
3325
|
-
const nodeStream = (0,
|
|
4228
|
+
const nodeStream = (0, import_node_fs17.createReadStream)(filePath);
|
|
3326
4229
|
const webStream = import_node_stream.Readable.toWeb(nodeStream);
|
|
3327
4230
|
const response = await fetch(presignedUrl, {
|
|
3328
4231
|
method: "PUT",
|
|
@@ -3615,35 +4518,35 @@ async function finalizeAndUpload(cloudClient, cloudConfig, testRunId, report, co
|
|
|
3615
4518
|
// src/report-orchestrator.ts
|
|
3616
4519
|
function matchVideoToFlow(flowFile, videoPaths, flowRecordingPath) {
|
|
3617
4520
|
if (videoPaths.length === 0) return null;
|
|
3618
|
-
const flowBase = (0,
|
|
4521
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3619
4522
|
const exact = videoPaths.find(
|
|
3620
|
-
(v) => (0,
|
|
4523
|
+
(v) => (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase() === flowBase
|
|
3621
4524
|
);
|
|
3622
4525
|
if (exact) return exact;
|
|
3623
4526
|
const forward = videoPaths.find(
|
|
3624
|
-
(v) => (0,
|
|
4527
|
+
(v) => (0, import_node_path16.basename)(v).toLowerCase().includes(flowBase)
|
|
3625
4528
|
);
|
|
3626
4529
|
if (forward) return forward;
|
|
3627
4530
|
const reverse = videoPaths.find((v) => {
|
|
3628
|
-
const videoBase = (0,
|
|
4531
|
+
const videoBase = (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase();
|
|
3629
4532
|
return videoBase.length >= 3 && (flowBase.startsWith(videoBase) || flowBase.includes(videoBase));
|
|
3630
4533
|
});
|
|
3631
4534
|
if (reverse) return reverse;
|
|
3632
4535
|
if (flowRecordingPath) {
|
|
3633
|
-
const recBase = (0,
|
|
3634
|
-
const byRec = videoPaths.find((v) => (0,
|
|
4536
|
+
const recBase = (0, import_node_path16.basename)(flowRecordingPath, (0, import_node_path16.extname)(flowRecordingPath)).toLowerCase();
|
|
4537
|
+
const byRec = videoPaths.find((v) => (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase() === recBase);
|
|
3635
4538
|
if (byRec) return byRec;
|
|
3636
|
-
const byRecPartial = videoPaths.find((v) => (0,
|
|
4539
|
+
const byRecPartial = videoPaths.find((v) => (0, import_node_path16.basename)(v).toLowerCase().includes(recBase));
|
|
3637
4540
|
if (byRecPartial) return byRecPartial;
|
|
3638
4541
|
}
|
|
3639
4542
|
return null;
|
|
3640
4543
|
}
|
|
3641
4544
|
function matchScreenshotsToFlow(flowFile, screenshotPaths) {
|
|
3642
4545
|
if (screenshotPaths.length === 0) return [];
|
|
3643
|
-
const flowBase = (0,
|
|
4546
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3644
4547
|
const matched = screenshotPaths.filter((s) => {
|
|
3645
|
-
const name = (0,
|
|
3646
|
-
const nameBase = (0,
|
|
4548
|
+
const name = (0, import_node_path16.basename)(s).toLowerCase();
|
|
4549
|
+
const nameBase = (0, import_node_path16.basename)(s, (0, import_node_path16.extname)(s)).toLowerCase();
|
|
3647
4550
|
if (name.startsWith(flowBase) || name.includes(flowBase)) return true;
|
|
3648
4551
|
return nameBase.length >= 3 && (flowBase.startsWith(nameBase) || flowBase.includes(nameBase));
|
|
3649
4552
|
});
|
|
@@ -3663,18 +4566,18 @@ function extractRecordingPath(commands) {
|
|
|
3663
4566
|
function buildFlowRecordingMap(commandSteps) {
|
|
3664
4567
|
const map = /* @__PURE__ */ new Map();
|
|
3665
4568
|
for (const [filePath, commands] of commandSteps) {
|
|
3666
|
-
const name = (0,
|
|
4569
|
+
const name = (0, import_node_path16.basename)(filePath).replace(/^commands[-_]?/i, "").replace(/\.json$/i, "").toLowerCase();
|
|
3667
4570
|
const recPath = extractRecordingPath(commands);
|
|
3668
4571
|
if (recPath && name) map.set(name, recPath);
|
|
3669
4572
|
}
|
|
3670
4573
|
return map;
|
|
3671
4574
|
}
|
|
3672
4575
|
function commandsForFlow(commandSteps, flowFile) {
|
|
3673
|
-
const flowBase = (0,
|
|
4576
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3674
4577
|
if (!flowBase || commandSteps.size === 0) {
|
|
3675
4578
|
return { commands: [], matchedAnyFile: false };
|
|
3676
4579
|
}
|
|
3677
|
-
const stripCmdPrefix = (p) => (0,
|
|
4580
|
+
const stripCmdPrefix = (p) => (0, import_node_path16.basename)(p, (0, import_node_path16.extname)(p)).replace(/^commands[-_]?/i, "").toLowerCase();
|
|
3678
4581
|
for (const [cmdFile, cmds] of commandSteps) {
|
|
3679
4582
|
if (stripCmdPrefix(cmdFile) === flowBase) {
|
|
3680
4583
|
return { commands: cmds, matchedAnyFile: true };
|
|
@@ -3706,7 +4609,7 @@ function platformToOs(platform) {
|
|
|
3706
4609
|
return void 0;
|
|
3707
4610
|
}
|
|
3708
4611
|
}
|
|
3709
|
-
function flowToTestResult(flow) {
|
|
4612
|
+
function flowToTestResult(flow, apiCalls) {
|
|
3710
4613
|
const consoleLogs = flow.logEntries.length > 0 ? flow.logEntries.map((entry) => ({
|
|
3711
4614
|
level: entry.level.toLowerCase(),
|
|
3712
4615
|
message: entry.message,
|
|
@@ -3733,7 +4636,8 @@ function flowToTestResult(flow) {
|
|
|
3733
4636
|
platform: flow.platform !== "unknown" ? flow.platform : void 0,
|
|
3734
4637
|
os: platformToOs(flow.platform),
|
|
3735
4638
|
deviceName: flow.deviceId,
|
|
3736
|
-
...consoleLogs ? { consoleLogs } : {}
|
|
4639
|
+
...consoleLogs ? { consoleLogs } : {},
|
|
4640
|
+
...apiCalls && apiCalls.length > 0 ? { apiCalls } : {}
|
|
3737
4641
|
};
|
|
3738
4642
|
}
|
|
3739
4643
|
function logEntriesForFlow(allEntries, flowStartedAt, flowCompletedAt) {
|
|
@@ -3752,7 +4656,7 @@ function logEntriesForFlow(allEntries, flowStartedAt, flowCompletedAt) {
|
|
|
3752
4656
|
async function orchestrateReport(input) {
|
|
3753
4657
|
const { config } = input;
|
|
3754
4658
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3755
|
-
const testRunId = config.testRunId ?? (0,
|
|
4659
|
+
const testRunId = config.testRunId ?? (0, import_node_crypto4.randomUUID)();
|
|
3756
4660
|
const artifacts = collectArtifacts(input.testOutputDir, input.debugOutputDir);
|
|
3757
4661
|
const junitPath = input.junitPath ?? artifacts.junitReportPath;
|
|
3758
4662
|
const logFiles = input.debugOutputDir ? discoverLogFiles(input.debugOutputDir) : artifacts.logPaths;
|
|
@@ -3767,7 +4671,7 @@ async function orchestrateReport(input) {
|
|
|
3767
4671
|
if (detected !== "unknown") platform = detected;
|
|
3768
4672
|
}
|
|
3769
4673
|
const flowMetadataMap = /* @__PURE__ */ new Map();
|
|
3770
|
-
if (input.flowsDir && (0,
|
|
4674
|
+
if (input.flowsDir && (0, import_node_fs18.existsSync)(input.flowsDir)) {
|
|
3771
4675
|
const flowFiles = discoverFlowFiles(input.flowsDir);
|
|
3772
4676
|
for (const flowFile of flowFiles) {
|
|
3773
4677
|
const meta = parseFlowFile(flowFile);
|
|
@@ -3788,7 +4692,7 @@ async function orchestrateReport(input) {
|
|
|
3788
4692
|
}
|
|
3789
4693
|
const flowRecordingMap = buildFlowRecordingMap(commandSteps);
|
|
3790
4694
|
const flowResults = [];
|
|
3791
|
-
if (junitPath && (0,
|
|
4695
|
+
if (junitPath && (0, import_node_fs18.existsSync)(junitPath)) {
|
|
3792
4696
|
const junit = parseJUnitFile(junitPath);
|
|
3793
4697
|
const allCommands = Array.from(commandSteps.values()).flat();
|
|
3794
4698
|
const allAssertions = allCommands.filter((c) => c.category === "assertion");
|
|
@@ -3802,7 +4706,7 @@ async function orchestrateReport(input) {
|
|
|
3802
4706
|
const flowStartedAt = startedAt;
|
|
3803
4707
|
const flowCompletedAt = new Date(new Date(flowStartedAt).getTime() + durationMs).toISOString();
|
|
3804
4708
|
const flowFile = testCase.classname || name;
|
|
3805
|
-
const flowBase = (0,
|
|
4709
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3806
4710
|
const recordingPath = flowRecordingMap.get(flowBase) ?? null;
|
|
3807
4711
|
const { commands: flowCmds, matchedAnyFile } = commandsForFlow(commandSteps, flowFile);
|
|
3808
4712
|
const perFlowCommands = matchedAnyFile ? flowCmds.filter((c) => c.category !== "assertion") : allNonAssertions;
|
|
@@ -3863,7 +4767,20 @@ async function orchestrateReport(input) {
|
|
|
3863
4767
|
});
|
|
3864
4768
|
}
|
|
3865
4769
|
}
|
|
3866
|
-
const
|
|
4770
|
+
const apiCallsByFlow = (() => {
|
|
4771
|
+
if (!config.network?.enabled && !config.network?.harPath) return void 0;
|
|
4772
|
+
const windows = flowResults.map((f) => ({
|
|
4773
|
+
testId: `${f.flowFile}::${f.flowName}`,
|
|
4774
|
+
startedAt: f.startedAt,
|
|
4775
|
+
completedAt: f.completedAt
|
|
4776
|
+
}));
|
|
4777
|
+
const bound = collectNetworkByFlow(input.networkJsonlDir ?? null, config.network, windows);
|
|
4778
|
+
if (bound.size === 0) return void 0;
|
|
4779
|
+
const remapped = /* @__PURE__ */ new Map();
|
|
4780
|
+
for (const [k, v] of bound) remapped.set(k, reindexFlowCalls(v));
|
|
4781
|
+
return remapped;
|
|
4782
|
+
})();
|
|
4783
|
+
const timeline = buildTimeline(flowResults, apiCallsByFlow);
|
|
3867
4784
|
const summary = buildSummary(timeline);
|
|
3868
4785
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3869
4786
|
const totalDuration = Date.now() - new Date(startedAt).getTime();
|
|
@@ -3879,12 +4796,12 @@ async function orchestrateReport(input) {
|
|
|
3879
4796
|
timeline,
|
|
3880
4797
|
shardRunIds: null
|
|
3881
4798
|
};
|
|
3882
|
-
(0,
|
|
3883
|
-
(0,
|
|
3884
|
-
generateHtmlReport(report, allAiDefects, artifacts.screenshotPaths, (0,
|
|
4799
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)((0, import_node_path16.resolve)(config.outputPath)), { recursive: true });
|
|
4800
|
+
(0, import_node_fs18.writeFileSync)((0, import_node_path16.resolve)(config.outputPath), JSON.stringify(report, null, 2), "utf-8");
|
|
4801
|
+
generateHtmlReport(report, allAiDefects, artifacts.screenshotPaths, (0, import_node_path16.resolve)(config.htmlReportPath));
|
|
3885
4802
|
printConsoleSummary(summary, config.outputPath, config.htmlReportPath, config.quiet, allAiDefects);
|
|
3886
4803
|
if (config.openReport) {
|
|
3887
|
-
openInBrowser((0,
|
|
4804
|
+
openInBrowser((0, import_node_path16.resolve)(config.htmlReportPath));
|
|
3888
4805
|
}
|
|
3889
4806
|
if (config.cloud) {
|
|
3890
4807
|
const cloudClient = new CloudClient(config.cloud);
|
|
@@ -3893,7 +4810,7 @@ async function orchestrateReport(input) {
|
|
|
3893
4810
|
const videoArtifacts = [];
|
|
3894
4811
|
if (videoPathsToUpload.length > 0) {
|
|
3895
4812
|
for (const flow of flowResults) {
|
|
3896
|
-
const flowBase = (0,
|
|
4813
|
+
const flowBase = (0, import_node_path16.basename)(flow.flowFile, (0, import_node_path16.extname)(flow.flowFile)).toLowerCase();
|
|
3897
4814
|
const recordingPath = flowRecordingMap.get(flowBase) ?? null;
|
|
3898
4815
|
const matched = matchVideoToFlow(flow.flowFile, videoPathsToUpload, recordingPath);
|
|
3899
4816
|
if (matched) {
|
|
@@ -3915,7 +4832,10 @@ async function orchestrateReport(input) {
|
|
|
3915
4832
|
}
|
|
3916
4833
|
const matchedPaths = new Set(videoArtifacts.map((va) => va.path));
|
|
3917
4834
|
const unmatchedVideos = videoPathsToUpload.filter((v) => !matchedPaths.has(v));
|
|
3918
|
-
const testsForUpload = flowResults.map(
|
|
4835
|
+
const testsForUpload = flowResults.map((f) => {
|
|
4836
|
+
const testId = `${f.flowFile}::${f.flowName}`;
|
|
4837
|
+
return flowToTestResult(f, apiCallsByFlow?.get(testId));
|
|
4838
|
+
});
|
|
3919
4839
|
await finalizeAndUpload(
|
|
3920
4840
|
cloudClient,
|
|
3921
4841
|
config.cloud,
|
|
@@ -3962,6 +4882,16 @@ Test Options:
|
|
|
3962
4882
|
-e, --env <KEY=VALUE> Environment variable
|
|
3963
4883
|
--quiet Suppress output
|
|
3964
4884
|
|
|
4885
|
+
Network Capture Options (mobile API logs):
|
|
4886
|
+
--capture-network Spawn mitmproxy to capture HTTP traffic from the app
|
|
4887
|
+
(requires mitmdump on PATH; --har-path is an alternative)
|
|
4888
|
+
--proxy-port <n> Proxy listen port (default: 8080)
|
|
4889
|
+
--proxy-host <host> Proxy listen host (default: 127.0.0.1)
|
|
4890
|
+
--har-path <file> Import a pre-recorded HAR file instead of running a proxy
|
|
4891
|
+
--skip-cert-install Skip auto-installing the mitmproxy CA on the device
|
|
4892
|
+
--proxy-include <pat> Only capture URLs matching this substring or regex (repeatable)
|
|
4893
|
+
--proxy-exclude <pat> Drop URLs matching this substring or regex (repeatable)
|
|
4894
|
+
|
|
3965
4895
|
Report Options:
|
|
3966
4896
|
--junit <path> Path to JUnit XML report
|
|
3967
4897
|
--artifacts <dir> Path to Maestro test output directory
|
|
@@ -4001,6 +4931,15 @@ function parseArgs(args) {
|
|
|
4001
4931
|
} else {
|
|
4002
4932
|
options["env"] = args[i];
|
|
4003
4933
|
}
|
|
4934
|
+
} else if (key === "proxy-include" || key === "proxy-exclude") {
|
|
4935
|
+
const existing = options[key];
|
|
4936
|
+
if (Array.isArray(existing)) {
|
|
4937
|
+
existing.push(args[i]);
|
|
4938
|
+
} else if (typeof existing === "string") {
|
|
4939
|
+
options[key] = [existing, args[i]];
|
|
4940
|
+
} else {
|
|
4941
|
+
options[key] = args[i];
|
|
4942
|
+
}
|
|
4004
4943
|
} else {
|
|
4005
4944
|
options[key] = args[i];
|
|
4006
4945
|
}
|
|
@@ -4036,11 +4975,30 @@ function parseEnvVars(raw) {
|
|
|
4036
4975
|
}
|
|
4037
4976
|
return env;
|
|
4038
4977
|
}
|
|
4978
|
+
function asStringArray(v) {
|
|
4979
|
+
if (!v) return [];
|
|
4980
|
+
return Array.isArray(v) ? v : [v];
|
|
4981
|
+
}
|
|
4982
|
+
function buildNetworkOptions(options) {
|
|
4983
|
+
const enabled = options["capture-network"] === "true";
|
|
4984
|
+
const harPath = typeof options["har-path"] === "string" ? options["har-path"] : void 0;
|
|
4985
|
+
if (!enabled && !harPath) return void 0;
|
|
4986
|
+
return {
|
|
4987
|
+
enabled,
|
|
4988
|
+
proxyPort: typeof options["proxy-port"] === "string" ? Number.parseInt(options["proxy-port"], 10) : void 0,
|
|
4989
|
+
proxyHost: typeof options["proxy-host"] === "string" ? options["proxy-host"] : void 0,
|
|
4990
|
+
harPath,
|
|
4991
|
+
skipCertInstall: options["skip-cert-install"] === "true",
|
|
4992
|
+
includeUrls: asStringArray(options["proxy-include"]),
|
|
4993
|
+
excludeUrls: asStringArray(options["proxy-exclude"])
|
|
4994
|
+
};
|
|
4995
|
+
}
|
|
4039
4996
|
async function handleTest(options, positional) {
|
|
4040
4997
|
if (positional.length === 0) {
|
|
4041
4998
|
process.stderr.write("Error: No flow paths specified. Usage: testrelic-maestro test <flow-paths...>\n");
|
|
4042
4999
|
process.exit(1);
|
|
4043
5000
|
}
|
|
5001
|
+
const network = buildNetworkOptions(options);
|
|
4044
5002
|
const testOptions = {
|
|
4045
5003
|
flowPaths: positional,
|
|
4046
5004
|
apiKey: typeof options["api-key"] === "string" ? options["api-key"] : void 0,
|
|
@@ -4054,12 +5012,10 @@ async function handleTest(options, positional) {
|
|
|
4054
5012
|
shards: typeof options["shards"] === "string" ? parseInt(options["shards"], 10) : void 0,
|
|
4055
5013
|
config: typeof options["config"] === "string" ? options["config"] : void 0,
|
|
4056
5014
|
env: parseEnvVars(options["env"]),
|
|
4057
|
-
quiet: options["quiet"] === "true"
|
|
5015
|
+
quiet: options["quiet"] === "true",
|
|
5016
|
+
network
|
|
4058
5017
|
};
|
|
4059
5018
|
process.stderr.write("\u2139 TestRelic: Running Maestro tests...\n");
|
|
4060
|
-
const runResult = await runMaestro(testOptions);
|
|
4061
|
-
process.stderr.write(`\u2139 TestRelic: Maestro exited with code ${runResult.exitCode}. Parsing results...
|
|
4062
|
-
`);
|
|
4063
5019
|
const config = resolveConfig({
|
|
4064
5020
|
outputPath: typeof options["output-dir"] === "string" ? `${options["output-dir"]}/testrelic-maestro.json` : void 0,
|
|
4065
5021
|
htmlReportPath: typeof options["html-report"] === "string" ? options["html-report"] : void 0,
|
|
@@ -4069,18 +5025,26 @@ async function handleTest(options, positional) {
|
|
|
4069
5025
|
endpoint: typeof options["endpoint"] === "string" ? options["endpoint"] : void 0
|
|
4070
5026
|
},
|
|
4071
5027
|
quiet: options["quiet"] === "true",
|
|
4072
|
-
flowsDir: positional[0]
|
|
5028
|
+
flowsDir: positional[0],
|
|
5029
|
+
network
|
|
4073
5030
|
});
|
|
5031
|
+
const runResult = await runMaestro(testOptions, { network: config.network });
|
|
5032
|
+
process.stderr.write(`\u2139 TestRelic: Maestro exited with code ${runResult.exitCode}. Parsing results...
|
|
5033
|
+
`);
|
|
4074
5034
|
await orchestrateReport({
|
|
4075
5035
|
junitPath: runResult.junitPath,
|
|
4076
5036
|
testOutputDir: runResult.testOutputDir,
|
|
4077
5037
|
debugOutputDir: runResult.debugOutputDir,
|
|
4078
5038
|
flowsDir: positional[0],
|
|
4079
|
-
config
|
|
5039
|
+
config,
|
|
5040
|
+
platform: testOptions.platform ?? "unknown",
|
|
5041
|
+
device: testOptions.device,
|
|
5042
|
+
networkJsonlDir: runResult.networkJsonlDir
|
|
4080
5043
|
});
|
|
4081
5044
|
process.exit(runResult.exitCode);
|
|
4082
5045
|
}
|
|
4083
5046
|
async function handleReport(options) {
|
|
5047
|
+
const network = buildNetworkOptions(options);
|
|
4084
5048
|
const config = resolveConfig({
|
|
4085
5049
|
outputPath: typeof options["output-dir"] === "string" ? `${options["output-dir"]}/testrelic-maestro.json` : void 0,
|
|
4086
5050
|
htmlReportPath: typeof options["html-report"] === "string" ? options["html-report"] : void 0,
|
|
@@ -4090,14 +5054,16 @@ async function handleReport(options) {
|
|
|
4090
5054
|
endpoint: typeof options["endpoint"] === "string" ? options["endpoint"] : void 0
|
|
4091
5055
|
},
|
|
4092
5056
|
quiet: options["quiet"] === "true",
|
|
4093
|
-
flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0
|
|
5057
|
+
flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0,
|
|
5058
|
+
network
|
|
4094
5059
|
});
|
|
4095
5060
|
process.stderr.write("\u2139 TestRelic: Parsing Maestro artifacts...\n");
|
|
4096
5061
|
await orchestrateReport({
|
|
4097
5062
|
junitPath: typeof options["junit"] === "string" ? options["junit"] : void 0,
|
|
4098
5063
|
testOutputDir: typeof options["artifacts"] === "string" ? options["artifacts"] : void 0,
|
|
4099
5064
|
flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0,
|
|
4100
|
-
config
|
|
5065
|
+
config,
|
|
5066
|
+
networkJsonlDir: typeof options["network-dir"] === "string" ? options["network-dir"] : null
|
|
4101
5067
|
});
|
|
4102
5068
|
}
|
|
4103
5069
|
async function main() {
|