@testrelic/maestro-analytics 1.1.0 → 1.2.0-next.54
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 +1199 -175
- package/dist/index.cjs +66 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -6
- package/dist/index.d.ts +118 -6
- 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
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
3
25
|
|
|
4
26
|
// src/config.ts
|
|
5
27
|
var import_core2 = require("@testrelic/core");
|
|
@@ -190,6 +212,37 @@ function mergeCloudConfig(fileConfig, reporterOptions) {
|
|
|
190
212
|
}
|
|
191
213
|
|
|
192
214
|
// src/config.ts
|
|
215
|
+
var DEFAULT_REDACT_HEADERS = [
|
|
216
|
+
"authorization",
|
|
217
|
+
"cookie",
|
|
218
|
+
"set-cookie",
|
|
219
|
+
"x-api-key"
|
|
220
|
+
];
|
|
221
|
+
var DEFAULT_REDACT_BODY_FIELDS = [
|
|
222
|
+
"password",
|
|
223
|
+
"secret",
|
|
224
|
+
"token",
|
|
225
|
+
"apiKey",
|
|
226
|
+
"api_key"
|
|
227
|
+
];
|
|
228
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
229
|
+
function resolveNetworkCapture(options) {
|
|
230
|
+
const o = options ?? {};
|
|
231
|
+
const envEnabled = process.env.TESTRELIC_CAPTURE_NETWORK === "1" || process.env.TESTRELIC_CAPTURE_NETWORK?.toLowerCase() === "true";
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
enabled: o.enabled ?? envEnabled ?? false,
|
|
234
|
+
proxyPort: o.proxyPort ?? (process.env.TESTRELIC_PROXY_PORT ? Number.parseInt(process.env.TESTRELIC_PROXY_PORT, 10) : 8080),
|
|
235
|
+
proxyHost: o.proxyHost ?? process.env.TESTRELIC_PROXY_HOST ?? "127.0.0.1",
|
|
236
|
+
harPath: o.harPath ?? process.env.TESTRELIC_HAR_PATH ?? null,
|
|
237
|
+
outputDir: o.outputDir ?? null,
|
|
238
|
+
skipCertInstall: o.skipCertInstall ?? false,
|
|
239
|
+
includeUrls: Object.freeze(o.includeUrls ? [...o.includeUrls] : []),
|
|
240
|
+
excludeUrls: Object.freeze(o.excludeUrls ? [...o.excludeUrls] : []),
|
|
241
|
+
redactHeaders: Object.freeze(o.redactHeaders ? [...o.redactHeaders] : [...DEFAULT_REDACT_HEADERS]),
|
|
242
|
+
redactBodyFields: Object.freeze(o.redactBodyFields ? [...o.redactBodyFields] : [...DEFAULT_REDACT_BODY_FIELDS]),
|
|
243
|
+
maxBodyBytes: o.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
244
|
+
});
|
|
245
|
+
}
|
|
193
246
|
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
194
247
|
function hasPrototypePollution(obj) {
|
|
195
248
|
if (typeof obj !== "object" || obj === null) return false;
|
|
@@ -237,6 +290,7 @@ function resolveConfig(options) {
|
|
|
237
290
|
target.quiet = options?.quiet ?? false;
|
|
238
291
|
target.reportMode = options?.reportMode ?? "batch";
|
|
239
292
|
target.cloud = resolveCloudFromMerge(options?.cloud ?? null);
|
|
293
|
+
target.network = resolveNetworkCapture(options?.network);
|
|
240
294
|
return Object.freeze(target);
|
|
241
295
|
}
|
|
242
296
|
function resolveCloudFromMerge(reporterOptions) {
|
|
@@ -251,14 +305,323 @@ function resolveCloudFromMerge(reporterOptions) {
|
|
|
251
305
|
}
|
|
252
306
|
|
|
253
307
|
// src/maestro-runner.ts
|
|
308
|
+
var import_node_child_process3 = require("child_process");
|
|
309
|
+
var import_node_fs4 = require("fs");
|
|
310
|
+
var import_node_path4 = require("path");
|
|
311
|
+
var import_node_os3 = require("os");
|
|
312
|
+
var import_node_crypto2 = require("crypto");
|
|
313
|
+
|
|
314
|
+
// src/network-proxy.ts
|
|
254
315
|
var import_node_child_process = require("child_process");
|
|
255
316
|
var import_node_fs2 = require("fs");
|
|
256
317
|
var import_node_path2 = require("path");
|
|
318
|
+
var import_node_url = require("url");
|
|
257
319
|
var import_node_os = require("os");
|
|
258
320
|
var import_node_crypto = require("crypto");
|
|
321
|
+
var import_meta = {};
|
|
322
|
+
function resolveAddonPath() {
|
|
323
|
+
const candidates = [];
|
|
324
|
+
try {
|
|
325
|
+
const here = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
326
|
+
candidates.push((0, import_node_path2.join)(here, "proxy-addon", "testrelic_capture.py"));
|
|
327
|
+
candidates.push((0, import_node_path2.join)(here, "..", "src", "proxy-addon", "testrelic_capture.py"));
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
const maybeDirname = globalThis.__dirname;
|
|
331
|
+
if (maybeDirname) {
|
|
332
|
+
candidates.push((0, import_node_path2.join)(maybeDirname, "proxy-addon", "testrelic_capture.py"));
|
|
333
|
+
}
|
|
334
|
+
for (const c of candidates) {
|
|
335
|
+
if ((0, import_node_fs2.existsSync)(c)) return (0, import_node_path2.resolve)(c);
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
function generateRunDir(base) {
|
|
340
|
+
if (base) {
|
|
341
|
+
(0, import_node_fs2.mkdirSync)(base, { recursive: true });
|
|
342
|
+
return base;
|
|
343
|
+
}
|
|
344
|
+
const id = (0, import_node_crypto.randomBytes)(6).toString("hex");
|
|
345
|
+
const dir = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `testrelic-maestro-network-${id}`);
|
|
346
|
+
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
347
|
+
return dir;
|
|
348
|
+
}
|
|
349
|
+
async function isMitmdumpAvailable() {
|
|
350
|
+
return new Promise((resolvePromise) => {
|
|
351
|
+
const child = (0, import_node_child_process.spawn)("mitmdump", ["--version"], { shell: true });
|
|
352
|
+
let resolved = false;
|
|
353
|
+
const settle = (ok) => {
|
|
354
|
+
if (resolved) return;
|
|
355
|
+
resolved = true;
|
|
356
|
+
resolvePromise(ok);
|
|
357
|
+
};
|
|
358
|
+
child.on("error", () => settle(false));
|
|
359
|
+
child.on("close", (code) => settle(code === 0));
|
|
360
|
+
setTimeout(() => {
|
|
361
|
+
try {
|
|
362
|
+
child.kill();
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
settle(false);
|
|
366
|
+
}, 5e3);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async function startNetworkProxy(options) {
|
|
370
|
+
const { config, verbose = true } = options;
|
|
371
|
+
if (!config.enabled) return null;
|
|
372
|
+
if (!await isMitmdumpAvailable()) {
|
|
373
|
+
if (verbose) {
|
|
374
|
+
process.stderr.write(
|
|
375
|
+
"\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"
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
const addon = resolveAddonPath();
|
|
381
|
+
if (!addon) {
|
|
382
|
+
if (verbose) {
|
|
383
|
+
process.stderr.write("\u26A0 TestRelic: network-capture addon not found in package; skipping capture.\n");
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const outputDir = generateRunDir(options.outputDir ?? config.outputDir ?? void 0);
|
|
388
|
+
const outputPath = (0, import_node_path2.join)(outputDir, "network.jsonl");
|
|
389
|
+
const args = [
|
|
390
|
+
"--listen-host",
|
|
391
|
+
config.proxyHost,
|
|
392
|
+
"--listen-port",
|
|
393
|
+
String(config.proxyPort),
|
|
394
|
+
"-s",
|
|
395
|
+
addon,
|
|
396
|
+
"--set",
|
|
397
|
+
`testrelic_output=${outputPath}`,
|
|
398
|
+
"--set",
|
|
399
|
+
`testrelic_redact_headers=${config.redactHeaders.join(",")}`,
|
|
400
|
+
"--set",
|
|
401
|
+
`testrelic_redact_body_fields=${config.redactBodyFields.join(",")}`,
|
|
402
|
+
"--set",
|
|
403
|
+
`testrelic_max_body_bytes=${config.maxBodyBytes}`,
|
|
404
|
+
// Reduce noise; the addon writes its own log lines on warn.
|
|
405
|
+
"--set",
|
|
406
|
+
"console_eventlog_verbosity=warn",
|
|
407
|
+
"--set",
|
|
408
|
+
"termlog_verbosity=warn"
|
|
409
|
+
];
|
|
410
|
+
let child;
|
|
411
|
+
try {
|
|
412
|
+
child = (0, import_node_child_process.spawn)("mitmdump", args, {
|
|
413
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
414
|
+
shell: true
|
|
415
|
+
});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (verbose) {
|
|
418
|
+
process.stderr.write(`\u26A0 TestRelic: failed to spawn mitmdump: ${err.message}
|
|
419
|
+
`);
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
child.stdout?.on("data", (chunk) => {
|
|
424
|
+
if (verbose) process.stderr.write(`[mitmdump] ${chunk.toString("utf-8")}`);
|
|
425
|
+
});
|
|
426
|
+
child.stderr?.on("data", (chunk) => {
|
|
427
|
+
if (verbose) process.stderr.write(`[mitmdump] ${chunk.toString("utf-8")}`);
|
|
428
|
+
});
|
|
429
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
430
|
+
const stop = () => new Promise((resolvePromise) => {
|
|
431
|
+
if (child.killed || child.exitCode !== null) {
|
|
432
|
+
resolvePromise();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
let settled = false;
|
|
436
|
+
const settle = () => {
|
|
437
|
+
if (settled) return;
|
|
438
|
+
settled = true;
|
|
439
|
+
resolvePromise();
|
|
440
|
+
};
|
|
441
|
+
child.once("close", settle);
|
|
442
|
+
child.once("exit", settle);
|
|
443
|
+
try {
|
|
444
|
+
child.kill("SIGINT");
|
|
445
|
+
} catch {
|
|
446
|
+
try {
|
|
447
|
+
child.kill();
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
setTimeout(() => {
|
|
452
|
+
if (!settled) {
|
|
453
|
+
try {
|
|
454
|
+
child.kill("SIGKILL");
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
settle();
|
|
458
|
+
}
|
|
459
|
+
}, 4e3);
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
outputPath,
|
|
463
|
+
outputDir,
|
|
464
|
+
host: config.proxyHost,
|
|
465
|
+
port: config.proxyPort,
|
|
466
|
+
stop
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/device-proxy-setup.ts
|
|
471
|
+
var import_node_child_process2 = require("child_process");
|
|
472
|
+
var import_node_path3 = require("path");
|
|
473
|
+
var import_node_os2 = require("os");
|
|
474
|
+
var import_node_fs3 = require("fs");
|
|
475
|
+
var NOOP_HANDLE = { teardown: async () => {
|
|
476
|
+
} };
|
|
477
|
+
function commandExists(cmd) {
|
|
478
|
+
const result = (0, import_node_child_process2.spawnSync)(cmd, ["--version"], { shell: true, stdio: "ignore" });
|
|
479
|
+
return result.error === void 0 || result.error.code !== "ENOENT";
|
|
480
|
+
}
|
|
481
|
+
function logHint(quiet, msg) {
|
|
482
|
+
if (!quiet) process.stderr.write(`${msg}
|
|
483
|
+
`);
|
|
484
|
+
}
|
|
485
|
+
function logManualInstructions(quiet, platform, proxyHost, proxyPort) {
|
|
486
|
+
if (quiet) return;
|
|
487
|
+
if (platform === "android") {
|
|
488
|
+
process.stderr.write(
|
|
489
|
+
`\u2139 TestRelic: real-device Android capture \u2014 set the device proxy manually:
|
|
490
|
+
Settings \u2192 Wi-Fi \u2192 (long-press your network) \u2192 Modify \u2192 Proxy: Manual
|
|
491
|
+
Host: ${proxyHost} Port: ${proxyPort}
|
|
492
|
+
Then install mitmproxy CA: open http://mitm.it from the device after the proxy is reachable.
|
|
493
|
+
`
|
|
494
|
+
);
|
|
495
|
+
} else if (platform === "ios") {
|
|
496
|
+
process.stderr.write(
|
|
497
|
+
`\u2139 TestRelic: real-device iOS capture \u2014 set the device proxy manually:
|
|
498
|
+
Settings \u2192 Wi-Fi \u2192 (i) on your network \u2192 Configure Proxy \u2192 Manual
|
|
499
|
+
Server: ${proxyHost} Port: ${proxyPort}
|
|
500
|
+
Then install + TRUST the mitmproxy CA:
|
|
501
|
+
1. Open http://mitm.it from Safari on the device \u2192 install profile.
|
|
502
|
+
2. Settings \u2192 General \u2192 About \u2192 Certificate Trust Settings \u2192 enable mitmproxy.
|
|
503
|
+
`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async function adb(args, deviceId) {
|
|
508
|
+
return new Promise((resolvePromise) => {
|
|
509
|
+
const full = deviceId ? ["-s", deviceId, ...args] : args;
|
|
510
|
+
const child = (0, import_node_child_process2.spawn)("adb", full, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
511
|
+
let out = "";
|
|
512
|
+
let err = "";
|
|
513
|
+
child.stdout?.on("data", (c) => {
|
|
514
|
+
out += c.toString();
|
|
515
|
+
});
|
|
516
|
+
child.stderr?.on("data", (c) => {
|
|
517
|
+
err += c.toString();
|
|
518
|
+
});
|
|
519
|
+
child.on("error", () => resolvePromise({ code: 127, stdout: out, stderr: err || "adb not on PATH" }));
|
|
520
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout: out, stderr: err }));
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async function setUpAndroidEmulator(opts) {
|
|
524
|
+
const { deviceId, proxyHost, proxyPort, quiet, skipCertInstall } = opts;
|
|
525
|
+
if (!commandExists("adb")) {
|
|
526
|
+
logHint(quiet, "\u26A0 TestRelic: `adb` not on PATH; skipping automatic Android proxy setup. See manual instructions:");
|
|
527
|
+
logManualInstructions(quiet, "android", proxyHost, proxyPort);
|
|
528
|
+
return NOOP_HANDLE;
|
|
529
|
+
}
|
|
530
|
+
const setResult = await adb(["shell", "settings", "put", "global", "http_proxy", `${proxyHost}:${proxyPort}`], deviceId);
|
|
531
|
+
if (setResult.code !== 0) {
|
|
532
|
+
logHint(quiet, `\u26A0 TestRelic: adb set proxy failed: ${setResult.stderr.trim() || "unknown error"}`);
|
|
533
|
+
logManualInstructions(quiet, "android", proxyHost, proxyPort);
|
|
534
|
+
return NOOP_HANDLE;
|
|
535
|
+
}
|
|
536
|
+
logHint(quiet, `\u2139 TestRelic: Android proxy set to ${proxyHost}:${proxyPort} on ${deviceId ?? "default device"}.`);
|
|
537
|
+
if (!skipCertInstall) {
|
|
538
|
+
logHint(
|
|
539
|
+
quiet,
|
|
540
|
+
"\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."
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const teardown = async () => {
|
|
544
|
+
await adb(["shell", "settings", "put", "global", "http_proxy", ":0"], deviceId);
|
|
545
|
+
};
|
|
546
|
+
return { teardown };
|
|
547
|
+
}
|
|
548
|
+
async function xcrun(args) {
|
|
549
|
+
return new Promise((resolvePromise) => {
|
|
550
|
+
const child = (0, import_node_child_process2.spawn)("xcrun", args, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
551
|
+
let out = "";
|
|
552
|
+
let err = "";
|
|
553
|
+
child.stdout?.on("data", (c) => {
|
|
554
|
+
out += c.toString();
|
|
555
|
+
});
|
|
556
|
+
child.stderr?.on("data", (c) => {
|
|
557
|
+
err += c.toString();
|
|
558
|
+
});
|
|
559
|
+
child.on("error", () => resolvePromise({ code: 127, stdout: out, stderr: err || "xcrun not on PATH" }));
|
|
560
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout: out, stderr: err }));
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async function setUpIosSimulator(opts) {
|
|
564
|
+
const { proxyHost, proxyPort, quiet, skipCertInstall } = opts;
|
|
565
|
+
if (process.platform !== "darwin") {
|
|
566
|
+
logHint(quiet, "\u26A0 TestRelic: iOS simulator capture requires macOS; the host is not Darwin. Skipping setup.");
|
|
567
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
568
|
+
return NOOP_HANDLE;
|
|
569
|
+
}
|
|
570
|
+
if (!commandExists("xcrun")) {
|
|
571
|
+
logHint(quiet, "\u26A0 TestRelic: `xcrun` not on PATH; skipping automatic iOS simulator setup.");
|
|
572
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
573
|
+
return NOOP_HANDLE;
|
|
574
|
+
}
|
|
575
|
+
if (!skipCertInstall) {
|
|
576
|
+
const caCandidate = (0, import_node_path3.join)(process.env.HOME ?? "", ".mitmproxy", "mitmproxy-ca-cert.pem");
|
|
577
|
+
if ((0, import_node_fs3.existsSync)(caCandidate)) {
|
|
578
|
+
const udid = opts.deviceId ?? "booted";
|
|
579
|
+
const res = await xcrun(["simctl", "keychain", udid, "add-root-cert", caCandidate]);
|
|
580
|
+
if (res.code === 0) {
|
|
581
|
+
logHint(quiet, `\u2139 TestRelic: installed mitmproxy CA on iOS simulator (${udid}).`);
|
|
582
|
+
} else {
|
|
583
|
+
logHint(quiet, `\u26A0 TestRelic: failed to add-root-cert via simctl: ${res.stderr.trim()}`);
|
|
584
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
logHint(
|
|
588
|
+
quiet,
|
|
589
|
+
`\u26A0 TestRelic: mitmproxy CA not found at ${caCandidate}. Run mitmdump once to generate it,
|
|
590
|
+
then re-run with --capture-network. Falling back to manual instructions:`
|
|
591
|
+
);
|
|
592
|
+
logManualInstructions(quiet, "ios", proxyHost, proxyPort);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
logHint(
|
|
596
|
+
quiet,
|
|
597
|
+
`\u2139 TestRelic: in the iOS simulator, configure Wi-Fi \u2192 (i) \u2192 Manual proxy:
|
|
598
|
+
Server: ${proxyHost} Port: ${proxyPort}
|
|
599
|
+
(We do not set host-wide \`networksetup\` proxy automatically; see docs.)`
|
|
600
|
+
);
|
|
601
|
+
return NOOP_HANDLE;
|
|
602
|
+
}
|
|
603
|
+
async function setUpDeviceProxy(options) {
|
|
604
|
+
switch (options.platform) {
|
|
605
|
+
case "android":
|
|
606
|
+
return setUpAndroidEmulator(options);
|
|
607
|
+
case "ios":
|
|
608
|
+
return setUpIosSimulator(options);
|
|
609
|
+
case "web":
|
|
610
|
+
logHint(options.quiet, "\u2139 TestRelic: --capture-network ignored for Maestro web flows (browser already handles HTTP).");
|
|
611
|
+
return NOOP_HANDLE;
|
|
612
|
+
case "unknown":
|
|
613
|
+
default:
|
|
614
|
+
logHint(options.quiet, "\u26A0 TestRelic: target platform unknown; printing manual proxy setup instructions:");
|
|
615
|
+
logManualInstructions(options.quiet, "android", options.proxyHost, options.proxyPort);
|
|
616
|
+
logManualInstructions(options.quiet, "ios", options.proxyHost, options.proxyPort);
|
|
617
|
+
return NOOP_HANDLE;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/maestro-runner.ts
|
|
259
622
|
function generateTempDir() {
|
|
260
|
-
const id = (0,
|
|
261
|
-
return (0,
|
|
623
|
+
const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
|
|
624
|
+
return (0, import_node_path4.join)((0, import_node_os3.tmpdir)(), `testrelic-maestro-${id}`);
|
|
262
625
|
}
|
|
263
626
|
function buildMaestroArgs(options, tempDir) {
|
|
264
627
|
const args = [];
|
|
@@ -269,9 +632,9 @@ function buildMaestroArgs(options, tempDir) {
|
|
|
269
632
|
args.push("--device", options.device);
|
|
270
633
|
}
|
|
271
634
|
args.push("test");
|
|
272
|
-
const junitPath = (0,
|
|
273
|
-
const testOutputDir = (0,
|
|
274
|
-
const debugOutputDir = (0,
|
|
635
|
+
const junitPath = (0, import_node_path4.join)(tempDir, "report.xml");
|
|
636
|
+
const testOutputDir = (0, import_node_path4.join)(tempDir, "artifacts");
|
|
637
|
+
const debugOutputDir = (0, import_node_path4.join)(tempDir, "debug");
|
|
275
638
|
args.push("--format", "junit");
|
|
276
639
|
args.push("--output", junitPath);
|
|
277
640
|
args.push("--test-output-dir", testOutputDir);
|
|
@@ -302,60 +665,124 @@ function buildMaestroArgs(options, tempDir) {
|
|
|
302
665
|
args.push(...options.flowPaths);
|
|
303
666
|
return args;
|
|
304
667
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
668
|
+
function normalisePlatform(p) {
|
|
669
|
+
switch ((p ?? "").toLowerCase()) {
|
|
670
|
+
case "android":
|
|
671
|
+
return "android";
|
|
672
|
+
case "ios":
|
|
673
|
+
return "ios";
|
|
674
|
+
case "web":
|
|
675
|
+
return "web";
|
|
676
|
+
default:
|
|
677
|
+
return "unknown";
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function runMaestroProc(args, junitPath, testOutputDir, debugOutputDir, networkJsonlDir, quiet, onClose) {
|
|
313
681
|
return new Promise((resolvePromise) => {
|
|
314
682
|
const stdoutChunks = [];
|
|
315
683
|
const stderrChunks = [];
|
|
316
|
-
const proc = (0,
|
|
684
|
+
const proc = (0, import_node_child_process3.spawn)("maestro", args, {
|
|
317
685
|
stdio: ["inherit", "pipe", "pipe"],
|
|
318
686
|
shell: true
|
|
319
687
|
});
|
|
320
688
|
proc.stdout.on("data", (chunk) => {
|
|
321
689
|
stdoutChunks.push(chunk);
|
|
322
|
-
if (!
|
|
690
|
+
if (!quiet) process.stdout.write(chunk);
|
|
323
691
|
});
|
|
324
692
|
proc.stderr.on("data", (chunk) => {
|
|
325
693
|
stderrChunks.push(chunk);
|
|
326
|
-
if (!
|
|
694
|
+
if (!quiet) process.stderr.write(chunk);
|
|
327
695
|
});
|
|
328
|
-
proc.on("close", (code) => {
|
|
696
|
+
proc.on("close", async (code) => {
|
|
697
|
+
try {
|
|
698
|
+
await onClose();
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
329
701
|
resolvePromise({
|
|
330
702
|
exitCode: code ?? 1,
|
|
331
703
|
junitPath,
|
|
332
704
|
testOutputDir,
|
|
333
705
|
debugOutputDir,
|
|
334
706
|
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
335
|
-
stderr: Buffer.concat(stderrChunks).toString("utf-8")
|
|
707
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
708
|
+
networkJsonlDir
|
|
336
709
|
});
|
|
337
710
|
});
|
|
338
|
-
proc.on("error", (err) => {
|
|
711
|
+
proc.on("error", async (err) => {
|
|
712
|
+
try {
|
|
713
|
+
await onClose();
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
339
716
|
resolvePromise({
|
|
340
717
|
exitCode: 127,
|
|
341
718
|
junitPath,
|
|
342
719
|
testOutputDir,
|
|
343
720
|
debugOutputDir,
|
|
344
721
|
stdout: "",
|
|
345
|
-
stderr: `Failed to start maestro CLI: ${err.message}
|
|
722
|
+
stderr: `Failed to start maestro CLI: ${err.message}`,
|
|
723
|
+
networkJsonlDir
|
|
346
724
|
});
|
|
347
725
|
});
|
|
348
726
|
});
|
|
349
727
|
}
|
|
728
|
+
async function runMaestro(options, extras = {}) {
|
|
729
|
+
const tempDir = options.outputDir ? (0, import_node_path4.resolve)(options.outputDir) : generateTempDir();
|
|
730
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.join)(tempDir, "artifacts"), { recursive: true });
|
|
731
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.join)(tempDir, "debug"), { recursive: true });
|
|
732
|
+
const args = buildMaestroArgs(options, tempDir);
|
|
733
|
+
const junitPath = (0, import_node_path4.join)(tempDir, "report.xml");
|
|
734
|
+
const testOutputDir = (0, import_node_path4.join)(tempDir, "artifacts");
|
|
735
|
+
const debugOutputDir = (0, import_node_path4.join)(tempDir, "debug");
|
|
736
|
+
let proxyHandle = null;
|
|
737
|
+
let deviceHandle = null;
|
|
738
|
+
const networkConfig = extras.network;
|
|
739
|
+
if (networkConfig?.enabled) {
|
|
740
|
+
const networkDir = (0, import_node_path4.join)(tempDir, "network");
|
|
741
|
+
proxyHandle = await startNetworkProxy({
|
|
742
|
+
config: networkConfig,
|
|
743
|
+
outputDir: networkDir,
|
|
744
|
+
verbose: !options.quiet
|
|
745
|
+
});
|
|
746
|
+
if (proxyHandle) {
|
|
747
|
+
deviceHandle = await setUpDeviceProxy({
|
|
748
|
+
platform: normalisePlatform(options.platform),
|
|
749
|
+
deviceId: options.device,
|
|
750
|
+
proxyHost: proxyHandle.host,
|
|
751
|
+
proxyPort: proxyHandle.port,
|
|
752
|
+
skipCertInstall: networkConfig.skipCertInstall,
|
|
753
|
+
quiet: options.quiet
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const networkJsonlDir = proxyHandle?.outputDir ?? null;
|
|
758
|
+
const cleanup = async () => {
|
|
759
|
+
if (proxyHandle) {
|
|
760
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
761
|
+
}
|
|
762
|
+
if (deviceHandle) {
|
|
763
|
+
try {
|
|
764
|
+
await deviceHandle.teardown();
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (proxyHandle) {
|
|
769
|
+
try {
|
|
770
|
+
await proxyHandle.stop();
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
return runMaestroProc(args, junitPath, testOutputDir, debugOutputDir, networkJsonlDir, options.quiet, cleanup);
|
|
776
|
+
}
|
|
350
777
|
|
|
351
778
|
// src/report-orchestrator.ts
|
|
352
|
-
var
|
|
353
|
-
var
|
|
354
|
-
var
|
|
779
|
+
var import_node_fs18 = require("fs");
|
|
780
|
+
var import_node_path16 = require("path");
|
|
781
|
+
var import_node_crypto4 = require("crypto");
|
|
355
782
|
|
|
356
783
|
// src/parsers/junit-parser.ts
|
|
357
784
|
var import_fast_xml_parser = require("fast-xml-parser");
|
|
358
|
-
var
|
|
785
|
+
var import_node_fs5 = require("fs");
|
|
359
786
|
var parser = new import_fast_xml_parser.XMLParser({
|
|
360
787
|
ignoreAttributes: false,
|
|
361
788
|
attributeNamePrefix: "@_",
|
|
@@ -453,13 +880,13 @@ function parseJUnitXml(xmlContent) {
|
|
|
453
880
|
return { testSuites: suites, totalTests, totalFailures, totalErrors, totalSkipped, totalTime };
|
|
454
881
|
}
|
|
455
882
|
function parseJUnitFile(filePath) {
|
|
456
|
-
const content = (0,
|
|
883
|
+
const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
|
|
457
884
|
return parseJUnitXml(content);
|
|
458
885
|
}
|
|
459
886
|
|
|
460
887
|
// src/parsers/command-parser.ts
|
|
461
|
-
var
|
|
462
|
-
var
|
|
888
|
+
var import_node_fs6 = require("fs");
|
|
889
|
+
var import_node_path5 = require("path");
|
|
463
890
|
var INTERACTION_COMMANDS = /* @__PURE__ */ new Set([
|
|
464
891
|
"tapOn",
|
|
465
892
|
"doubleTapOn",
|
|
@@ -524,6 +951,9 @@ var AI_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
524
951
|
"assertNoDefectsWithAI",
|
|
525
952
|
"extractTextWithAI"
|
|
526
953
|
]);
|
|
954
|
+
var REDACTED_DETAIL_COMMANDS = /* @__PURE__ */ new Set(["inputText", "pasteText"]);
|
|
955
|
+
var MAX_SCRIPT_DETAIL_LENGTH = 200;
|
|
956
|
+
var MAX_AI_PROMPT_LENGTH = 400;
|
|
527
957
|
function categorizeCommand(command) {
|
|
528
958
|
if (AI_COMMANDS.has(command)) return "ai";
|
|
529
959
|
if (ASSERTION_COMMANDS.has(command)) return "assertion";
|
|
@@ -564,22 +994,165 @@ function extractMaestroSelector(params) {
|
|
|
564
994
|
if (typeof params.path === "string") return params.path;
|
|
565
995
|
return void 0;
|
|
566
996
|
}
|
|
997
|
+
function truncate(value, max) {
|
|
998
|
+
if (value.length <= max) return value;
|
|
999
|
+
return `${value.slice(0, max)}\u2026`;
|
|
1000
|
+
}
|
|
1001
|
+
function redactText(value) {
|
|
1002
|
+
if (value.length === 0) return "";
|
|
1003
|
+
return `[REDACTED ${value.length} chars]`;
|
|
1004
|
+
}
|
|
1005
|
+
function stringifyPoint(point) {
|
|
1006
|
+
if (!point || typeof point !== "object") return void 0;
|
|
1007
|
+
const p = point;
|
|
1008
|
+
if (typeof p.x === "number" && typeof p.y === "number") return `${p.x},${p.y}`;
|
|
1009
|
+
return void 0;
|
|
1010
|
+
}
|
|
1011
|
+
function extractCommandDetails(commandName, params) {
|
|
1012
|
+
const details = {};
|
|
1013
|
+
switch (commandName) {
|
|
1014
|
+
case "inputText":
|
|
1015
|
+
case "pasteText": {
|
|
1016
|
+
const text = params.text ?? params.value;
|
|
1017
|
+
if (typeof text === "string") {
|
|
1018
|
+
details.text = REDACTED_DETAIL_COMMANDS.has(commandName) ? redactText(text) : text;
|
|
1019
|
+
}
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
case "swipe":
|
|
1023
|
+
case "scroll":
|
|
1024
|
+
case "scrollUntilVisible": {
|
|
1025
|
+
if (typeof params.direction === "string") details.direction = params.direction;
|
|
1026
|
+
const startPoint = stringifyPoint(params.start ?? params.startRelative);
|
|
1027
|
+
const endPoint = stringifyPoint(params.end ?? params.endRelative);
|
|
1028
|
+
if (startPoint) details.start = startPoint;
|
|
1029
|
+
if (endPoint) details.end = endPoint;
|
|
1030
|
+
if (typeof params.duration === "number") details.swipeDurationMs = params.duration;
|
|
1031
|
+
if (typeof params.timeout === "number") details.timeoutMs = params.timeout;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
case "setLocation": {
|
|
1035
|
+
if (typeof params.latitude === "number") details.latitude = params.latitude;
|
|
1036
|
+
if (typeof params.longitude === "number") details.longitude = params.longitude;
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
case "pressKey":
|
|
1040
|
+
case "back": {
|
|
1041
|
+
if (typeof params.key === "string") details.key = params.key;
|
|
1042
|
+
else if (typeof params.code === "string") details.key = params.code;
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
case "setOrientation": {
|
|
1046
|
+
if (typeof params.orientation === "string") details.orientation = params.orientation;
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
case "setAirplaneMode":
|
|
1050
|
+
case "toggleAirplaneMode": {
|
|
1051
|
+
if (typeof params.value === "string" || typeof params.value === "boolean") {
|
|
1052
|
+
details.value = params.value;
|
|
1053
|
+
}
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
case "setPermissions": {
|
|
1057
|
+
if (params.permissions && typeof params.permissions === "object") {
|
|
1058
|
+
details.permissions = params.permissions;
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
case "addMedia": {
|
|
1063
|
+
if (Array.isArray(params.mediaPaths)) details.mediaPaths = params.mediaPaths;
|
|
1064
|
+
else if (typeof params.path === "string") details.mediaPaths = [params.path];
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case "launchApp":
|
|
1068
|
+
case "killApp":
|
|
1069
|
+
case "stopApp":
|
|
1070
|
+
case "clearState": {
|
|
1071
|
+
const appId = params.appId ?? params.packageName ?? params.bundleId;
|
|
1072
|
+
if (typeof appId === "string") details.appId = appId;
|
|
1073
|
+
if (params.arguments) details.arguments = params.arguments;
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
case "openLink": {
|
|
1077
|
+
if (typeof params.url === "string") details.url = params.url;
|
|
1078
|
+
else if (typeof params.link === "string") details.url = params.link;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
case "runScript":
|
|
1082
|
+
case "evalScript": {
|
|
1083
|
+
if (typeof params.path === "string") details.path = params.path;
|
|
1084
|
+
if (typeof params.script === "string") details.script = truncate(params.script, MAX_SCRIPT_DETAIL_LENGTH);
|
|
1085
|
+
if (typeof params.sourceDescription === "string") details.sourceDescription = params.sourceDescription;
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
case "assertWithAI":
|
|
1089
|
+
case "assertNoDefectsWithAI":
|
|
1090
|
+
case "extractTextWithAI": {
|
|
1091
|
+
const prompt = params.assertion ?? params.prompt ?? params.question;
|
|
1092
|
+
if (typeof prompt === "string") details.prompt = truncate(prompt, MAX_AI_PROMPT_LENGTH);
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
case "extendedWaitUntil":
|
|
1096
|
+
case "waitForAnimationToEnd": {
|
|
1097
|
+
if (typeof params.timeout === "number") details.timeoutMs = params.timeout;
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
case "repeat":
|
|
1101
|
+
case "retry": {
|
|
1102
|
+
if (typeof params.times === "number") details.times = params.times;
|
|
1103
|
+
if (typeof params.maxRetries === "number") details.maxRetries = params.maxRetries;
|
|
1104
|
+
if (typeof params.condition === "object" && params.condition) details.condition = params.condition;
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "runFlow": {
|
|
1108
|
+
if (typeof params.file === "string") details.file = params.file;
|
|
1109
|
+
if (typeof params.flowId === "string") details.flowId = params.flowId;
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
default:
|
|
1113
|
+
for (const key of ["text", "value", "url"]) {
|
|
1114
|
+
const v = params[key];
|
|
1115
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
1116
|
+
details[key] = v;
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return Object.keys(details).length > 0 ? details : void 0;
|
|
1122
|
+
}
|
|
1123
|
+
function extractChildCommands(params, raw, baseTimestamp) {
|
|
1124
|
+
const candidates = [params.commands, raw.commands, raw.subSteps, raw.children];
|
|
1125
|
+
for (const c of candidates) {
|
|
1126
|
+
if (Array.isArray(c) && c.length > 0) {
|
|
1127
|
+
return c.map(
|
|
1128
|
+
(entry, i) => parseRawCommand(entry, i, baseTimestamp)
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
567
1134
|
function parseRawCommand(raw, index, baseTimestamp) {
|
|
568
1135
|
let command;
|
|
569
1136
|
let selector;
|
|
1137
|
+
let details;
|
|
1138
|
+
let children = [];
|
|
570
1139
|
if (typeof raw.command === "object" && raw.command !== null && !Array.isArray(raw.command)) {
|
|
571
1140
|
const commandObj = raw.command;
|
|
572
1141
|
const rawKey = Object.keys(commandObj)[0] ?? `step-${index}`;
|
|
573
1142
|
command = normalizeMaestroCommandName(rawKey);
|
|
574
1143
|
const params = commandObj[rawKey] ?? {};
|
|
575
1144
|
selector = extractMaestroSelector(params);
|
|
1145
|
+
details = extractCommandDetails(command, params);
|
|
1146
|
+
children = extractChildCommands(params, raw, baseTimestamp);
|
|
576
1147
|
} else {
|
|
577
1148
|
command = raw.command ?? raw.commandName ?? raw.name ?? `step-${index}`;
|
|
578
1149
|
selector = raw.selector ?? void 0;
|
|
1150
|
+
children = extractChildCommands({}, raw, baseTimestamp);
|
|
579
1151
|
}
|
|
580
1152
|
const meta = raw.metadata ?? {};
|
|
581
1153
|
const status = mapStatus(meta.status ?? raw.status);
|
|
582
1154
|
const duration = meta.duration ?? raw.duration ?? raw.durationMs ?? 0;
|
|
1155
|
+
const sequenceNumber = typeof meta.sequenceNumber === "number" ? meta.sequenceNumber : void 0;
|
|
583
1156
|
let timestamp;
|
|
584
1157
|
if (typeof meta.timestamp === "number") {
|
|
585
1158
|
timestamp = new Date(meta.timestamp).toISOString();
|
|
@@ -594,7 +1167,10 @@ function parseRawCommand(raw, index, baseTimestamp) {
|
|
|
594
1167
|
duration,
|
|
595
1168
|
timestamp,
|
|
596
1169
|
...selector ? { selector } : {},
|
|
597
|
-
...error ? { error } : {}
|
|
1170
|
+
...error ? { error } : {},
|
|
1171
|
+
...details ? { details } : {},
|
|
1172
|
+
...sequenceNumber !== void 0 ? { sequenceNumber } : {},
|
|
1173
|
+
...children.length > 0 ? { children } : {}
|
|
598
1174
|
};
|
|
599
1175
|
}
|
|
600
1176
|
function mapStatus(status) {
|
|
@@ -625,24 +1201,24 @@ function parseCommandsJson(jsonContent, baseTimestamp) {
|
|
|
625
1201
|
}
|
|
626
1202
|
function parseCommandsFile(filePath, baseTimestamp) {
|
|
627
1203
|
try {
|
|
628
|
-
const content = (0,
|
|
1204
|
+
const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
|
|
629
1205
|
return parseCommandsJson(content, baseTimestamp);
|
|
630
1206
|
} catch {
|
|
631
1207
|
return [];
|
|
632
1208
|
}
|
|
633
1209
|
}
|
|
634
1210
|
function discoverCommandFiles(artifactsDir) {
|
|
635
|
-
if (!(0,
|
|
1211
|
+
if (!(0, import_node_fs6.existsSync)(artifactsDir)) return [];
|
|
636
1212
|
try {
|
|
637
|
-
return (0,
|
|
1213
|
+
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
1214
|
} catch {
|
|
639
1215
|
return [];
|
|
640
1216
|
}
|
|
641
1217
|
}
|
|
642
1218
|
|
|
643
1219
|
// src/parsers/flow-parser.ts
|
|
644
|
-
var
|
|
645
|
-
var
|
|
1220
|
+
var import_node_fs7 = require("fs");
|
|
1221
|
+
var import_node_path6 = require("path");
|
|
646
1222
|
var import_yaml = require("yaml");
|
|
647
1223
|
function extractSubflowRefs(body) {
|
|
648
1224
|
const refs = [];
|
|
@@ -736,21 +1312,21 @@ function parseFlowYaml(content, filePath) {
|
|
|
736
1312
|
};
|
|
737
1313
|
}
|
|
738
1314
|
function parseFlowFile(filePath) {
|
|
739
|
-
const content = (0,
|
|
1315
|
+
const content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
|
|
740
1316
|
return parseFlowYaml(content, filePath);
|
|
741
1317
|
}
|
|
742
1318
|
function discoverFlowFiles(dir) {
|
|
743
|
-
if (!(0,
|
|
1319
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
744
1320
|
const results = [];
|
|
745
1321
|
function walk(currentDir) {
|
|
746
1322
|
try {
|
|
747
|
-
const entries = (0,
|
|
1323
|
+
const entries = (0, import_node_fs7.readdirSync)(currentDir, { withFileTypes: true });
|
|
748
1324
|
for (const entry of entries) {
|
|
749
|
-
const fullPath = (0,
|
|
1325
|
+
const fullPath = (0, import_node_path6.join)(currentDir, entry.name);
|
|
750
1326
|
if (entry.isDirectory()) {
|
|
751
1327
|
walk(fullPath);
|
|
752
1328
|
} else if (entry.isFile()) {
|
|
753
|
-
const ext = (0,
|
|
1329
|
+
const ext = (0, import_node_path6.extname)(entry.name).toLowerCase();
|
|
754
1330
|
if (ext === ".yaml" || ext === ".yml") {
|
|
755
1331
|
results.push(fullPath);
|
|
756
1332
|
}
|
|
@@ -764,11 +1340,11 @@ function discoverFlowFiles(dir) {
|
|
|
764
1340
|
}
|
|
765
1341
|
|
|
766
1342
|
// src/parsers/log-parser.ts
|
|
767
|
-
var
|
|
768
|
-
var
|
|
769
|
-
var
|
|
770
|
-
var LOG_LINE_REGEX = /^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s
|
|
771
|
-
var TIMESTAMP_ONLY_REGEX = /^\[?(\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]?\s
|
|
1343
|
+
var import_node_fs8 = require("fs");
|
|
1344
|
+
var import_node_path7 = require("path");
|
|
1345
|
+
var import_node_fs9 = require("fs");
|
|
1346
|
+
var LOG_LINE_REGEX = /^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+\[?\s*(\w+)\s*\]?\s+(.+)$/;
|
|
1347
|
+
var TIMESTAMP_ONLY_REGEX = /^\[?(\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]?\s+\[?\s*(\w+)\s*\]?\s+(.+)$/;
|
|
772
1348
|
function parseLevel(raw) {
|
|
773
1349
|
const upper = raw.toUpperCase();
|
|
774
1350
|
if (upper === "DEBUG" || upper === "TRACE" || upper === "VERBOSE") return "DEBUG";
|
|
@@ -777,10 +1353,31 @@ function parseLevel(raw) {
|
|
|
777
1353
|
if (upper === "ERROR" || upper === "FATAL" || upper === "SEVERE") return "ERROR";
|
|
778
1354
|
return "INFO";
|
|
779
1355
|
}
|
|
780
|
-
function
|
|
1356
|
+
function buildEntryTimestamp(dateAnchor, hhmmss) {
|
|
1357
|
+
const m = /^(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/.exec(hhmmss);
|
|
1358
|
+
if (!m) {
|
|
1359
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1360
|
+
}
|
|
1361
|
+
const h = Number(m[1]);
|
|
1362
|
+
const mi = Number(m[2]);
|
|
1363
|
+
const s = Number(m[3]);
|
|
1364
|
+
const ms = m[4] ? Number(m[4].padEnd(3, "0")) : 0;
|
|
1365
|
+
const local = new Date(
|
|
1366
|
+
dateAnchor.getFullYear(),
|
|
1367
|
+
dateAnchor.getMonth(),
|
|
1368
|
+
dateAnchor.getDate(),
|
|
1369
|
+
h,
|
|
1370
|
+
mi,
|
|
1371
|
+
s,
|
|
1372
|
+
ms
|
|
1373
|
+
);
|
|
1374
|
+
return local.toISOString();
|
|
1375
|
+
}
|
|
1376
|
+
function parseLogContent(content, defaultDate) {
|
|
781
1377
|
const entries = [];
|
|
782
1378
|
const lines = content.split("\n");
|
|
783
|
-
const
|
|
1379
|
+
const anchorDate = defaultDate instanceof Date ? defaultDate : null;
|
|
1380
|
+
const dateAnchorStr = typeof defaultDate === "string" ? defaultDate : defaultDate instanceof Date ? `${defaultDate.getFullYear()}-${String(defaultDate.getMonth() + 1).padStart(2, "0")}-${String(defaultDate.getDate()).padStart(2, "0")}` : (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
784
1381
|
for (const line2 of lines) {
|
|
785
1382
|
const trimmed = line2.trim();
|
|
786
1383
|
if (!trimmed) continue;
|
|
@@ -796,8 +1393,9 @@ function parseLogContent(content) {
|
|
|
796
1393
|
}
|
|
797
1394
|
match = TIMESTAMP_ONLY_REGEX.exec(trimmed);
|
|
798
1395
|
if (match) {
|
|
1396
|
+
const ts = anchorDate ? buildEntryTimestamp(anchorDate, match[1]) : `${dateAnchorStr}T${match[1]}`;
|
|
799
1397
|
entries.push({
|
|
800
|
-
timestamp:
|
|
1398
|
+
timestamp: ts,
|
|
801
1399
|
level: parseLevel(match[2]),
|
|
802
1400
|
message: match[3],
|
|
803
1401
|
source: null
|
|
@@ -814,10 +1412,10 @@ function parseLogContent(content) {
|
|
|
814
1412
|
}
|
|
815
1413
|
return entries;
|
|
816
1414
|
}
|
|
817
|
-
function parseLogFile(filePath) {
|
|
1415
|
+
function parseLogFile(filePath, defaultDate) {
|
|
818
1416
|
try {
|
|
819
|
-
const content = (0,
|
|
820
|
-
return parseLogContent(content);
|
|
1417
|
+
const content = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
|
|
1418
|
+
return parseLogContent(content, defaultDate);
|
|
821
1419
|
} catch {
|
|
822
1420
|
return [];
|
|
823
1421
|
}
|
|
@@ -838,17 +1436,17 @@ function detectPlatformFromLogs(entries) {
|
|
|
838
1436
|
return "unknown";
|
|
839
1437
|
}
|
|
840
1438
|
function discoverLogFiles(dir) {
|
|
841
|
-
if (!(0,
|
|
1439
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
842
1440
|
try {
|
|
843
|
-
return (0,
|
|
1441
|
+
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
1442
|
} catch {
|
|
845
1443
|
return [];
|
|
846
1444
|
}
|
|
847
1445
|
}
|
|
848
1446
|
|
|
849
1447
|
// src/parsers/ai-report-parser.ts
|
|
850
|
-
var
|
|
851
|
-
var
|
|
1448
|
+
var import_node_fs10 = require("fs");
|
|
1449
|
+
var import_node_path8 = require("path");
|
|
852
1450
|
var DEFECT_SECTION_REGEX = /(?:defect|issue|bug|error|warning|problem)/i;
|
|
853
1451
|
var SEVERITY_CRITICAL_REGEX = /(?:critical|severe|major|blocker)/i;
|
|
854
1452
|
var SEVERITY_WARNING_REGEX = /(?:warning|moderate|minor)/i;
|
|
@@ -935,38 +1533,250 @@ function parseAiReportHtml(htmlContent) {
|
|
|
935
1533
|
}
|
|
936
1534
|
function parseAiReportFile(filePath) {
|
|
937
1535
|
try {
|
|
938
|
-
const content = (0,
|
|
1536
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
939
1537
|
return parseAiReportHtml(content);
|
|
940
1538
|
} catch {
|
|
941
1539
|
return { defects: [], totalDefects: 0, hasIssues: false, rawHtml: null };
|
|
942
1540
|
}
|
|
943
1541
|
}
|
|
944
1542
|
function discoverAiReports(dir) {
|
|
945
|
-
if (!(0,
|
|
1543
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
946
1544
|
try {
|
|
947
|
-
return (0,
|
|
948
|
-
const name = (0,
|
|
949
|
-
const ext = (0,
|
|
1545
|
+
return (0, import_node_fs10.readdirSync)(dir, { recursive: true }).map(String).filter((f) => {
|
|
1546
|
+
const name = (0, import_node_path8.basename)(f).toLowerCase();
|
|
1547
|
+
const ext = (0, import_node_path8.extname)(f).toLowerCase();
|
|
950
1548
|
return ext === ".html" && (name.includes("insight") || name.includes("ai") || name.includes("analysis"));
|
|
951
|
-
}).map((f) => (0,
|
|
1549
|
+
}).map((f) => (0, import_node_path8.join)(dir, f));
|
|
952
1550
|
} catch {
|
|
953
1551
|
return [];
|
|
954
1552
|
}
|
|
955
1553
|
}
|
|
956
1554
|
|
|
1555
|
+
// src/parsers/network-parser.ts
|
|
1556
|
+
var import_node_fs11 = require("fs");
|
|
1557
|
+
var import_node_path9 = require("path");
|
|
1558
|
+
|
|
1559
|
+
// src/api-redactor.ts
|
|
1560
|
+
var REDACTED = "[REDACTED]";
|
|
1561
|
+
function redactHeaders(headers, redactList) {
|
|
1562
|
+
if (!headers) return null;
|
|
1563
|
+
const lower = new Set(redactList.map((h) => h.toLowerCase()));
|
|
1564
|
+
const out = {};
|
|
1565
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1566
|
+
out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
|
|
1567
|
+
}
|
|
1568
|
+
return out;
|
|
1569
|
+
}
|
|
1570
|
+
function redactJsonBody(value, fields) {
|
|
1571
|
+
if (value === null || typeof value !== "object") return value;
|
|
1572
|
+
if (Array.isArray(value)) return value.map((v) => redactJsonBody(v, fields));
|
|
1573
|
+
const out = {};
|
|
1574
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1575
|
+
out[k] = fields.has(k) ? REDACTED : redactJsonBody(v, fields);
|
|
1576
|
+
}
|
|
1577
|
+
return out;
|
|
1578
|
+
}
|
|
1579
|
+
function redactBody(body, fields) {
|
|
1580
|
+
if (!body || fields.length === 0) return body;
|
|
1581
|
+
try {
|
|
1582
|
+
const parsed = JSON.parse(body);
|
|
1583
|
+
const redacted = redactJsonBody(parsed, new Set(fields));
|
|
1584
|
+
return JSON.stringify(redacted);
|
|
1585
|
+
} catch {
|
|
1586
|
+
return body;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function redactApiCall(call, redactHeadersList, redactBodyFields) {
|
|
1590
|
+
return {
|
|
1591
|
+
...call,
|
|
1592
|
+
requestHeaders: redactHeaders(call.requestHeaders, redactHeadersList),
|
|
1593
|
+
responseHeaders: redactHeaders(call.responseHeaders, redactHeadersList),
|
|
1594
|
+
requestBody: redactBody(call.requestBody, redactBodyFields),
|
|
1595
|
+
responseBody: call.isBinary ? call.responseBody : redactBody(call.responseBody, redactBodyFields)
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
function matchesUrlFilter(url, include, exclude) {
|
|
1599
|
+
const matches = (pat) => typeof pat === "string" ? url.includes(pat) : pat.test(url);
|
|
1600
|
+
if (exclude.some(matches)) return false;
|
|
1601
|
+
if (include.length === 0) return true;
|
|
1602
|
+
return include.some(matches);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/parsers/network-parser.ts
|
|
1606
|
+
function parseJsonlLine(line2) {
|
|
1607
|
+
const trimmed = line2.trim();
|
|
1608
|
+
if (!trimmed) return null;
|
|
1609
|
+
try {
|
|
1610
|
+
const obj = JSON.parse(trimmed);
|
|
1611
|
+
if (typeof obj.url !== "string" || typeof obj.method !== "string") return null;
|
|
1612
|
+
return {
|
|
1613
|
+
id: obj.id ?? `api-call-${Date.now()}`,
|
|
1614
|
+
timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1615
|
+
method: obj.method,
|
|
1616
|
+
url: obj.url,
|
|
1617
|
+
requestHeaders: obj.requestHeaders ?? null,
|
|
1618
|
+
requestBody: obj.requestBody ?? null,
|
|
1619
|
+
responseStatusCode: obj.responseStatusCode ?? null,
|
|
1620
|
+
responseStatusText: obj.responseStatusText ?? null,
|
|
1621
|
+
responseHeaders: obj.responseHeaders ?? null,
|
|
1622
|
+
responseBody: obj.responseBody ?? null,
|
|
1623
|
+
responseTimeMs: typeof obj.responseTimeMs === "number" ? obj.responseTimeMs : 0,
|
|
1624
|
+
isBinary: obj.isBinary ?? false,
|
|
1625
|
+
error: obj.error ?? null
|
|
1626
|
+
};
|
|
1627
|
+
} catch {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
function parseNetworkJsonl(content, config) {
|
|
1632
|
+
const records = [];
|
|
1633
|
+
for (const line2 of content.split(/\r?\n/)) {
|
|
1634
|
+
const rec = parseJsonlLine(line2);
|
|
1635
|
+
if (!rec) continue;
|
|
1636
|
+
if (!matchesUrlFilter(rec.url, config.includeUrls, config.excludeUrls)) continue;
|
|
1637
|
+
records.push(redactApiCall(rec, config.redactHeaders, config.redactBodyFields));
|
|
1638
|
+
}
|
|
1639
|
+
return records;
|
|
1640
|
+
}
|
|
1641
|
+
function parseNetworkJsonlFile(filePath, config) {
|
|
1642
|
+
try {
|
|
1643
|
+
return parseNetworkJsonl((0, import_node_fs11.readFileSync)(filePath, "utf-8"), config);
|
|
1644
|
+
} catch {
|
|
1645
|
+
return [];
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function discoverNetworkJsonlFiles(dir) {
|
|
1649
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return [];
|
|
1650
|
+
try {
|
|
1651
|
+
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));
|
|
1652
|
+
} catch {
|
|
1653
|
+
return [];
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function harHeadersToRecord(headers) {
|
|
1657
|
+
if (!headers || headers.length === 0) return null;
|
|
1658
|
+
const out = {};
|
|
1659
|
+
for (const h of headers) {
|
|
1660
|
+
if (typeof h.name === "string" && typeof h.value === "string") out[h.name] = h.value;
|
|
1661
|
+
}
|
|
1662
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
1663
|
+
}
|
|
1664
|
+
function harBodyToString(postData) {
|
|
1665
|
+
if (!postData) return null;
|
|
1666
|
+
if (typeof postData.text === "string") return postData.text;
|
|
1667
|
+
if (Array.isArray(postData.params)) {
|
|
1668
|
+
return postData.params.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value ?? "")}`).join("&");
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
function isBinaryMime(mime) {
|
|
1673
|
+
if (!mime) return false;
|
|
1674
|
+
return /^(image|audio|video|application\/(octet-stream|pdf|zip|protobuf))/.test(mime);
|
|
1675
|
+
}
|
|
1676
|
+
function harEntryToRecord(entry, index) {
|
|
1677
|
+
const req = entry.request;
|
|
1678
|
+
const res = entry.response;
|
|
1679
|
+
if (!req?.method || !req.url) return null;
|
|
1680
|
+
const content = res?.content;
|
|
1681
|
+
const isBinary = isBinaryMime(content?.mimeType);
|
|
1682
|
+
return {
|
|
1683
|
+
id: `har-call-${index}`,
|
|
1684
|
+
timestamp: entry.startedDateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1685
|
+
method: req.method,
|
|
1686
|
+
url: req.url,
|
|
1687
|
+
requestHeaders: harHeadersToRecord(req.headers),
|
|
1688
|
+
requestBody: harBodyToString(req.postData),
|
|
1689
|
+
responseStatusCode: typeof res?.status === "number" && res.status > 0 ? res.status : null,
|
|
1690
|
+
responseStatusText: res?.statusText ?? null,
|
|
1691
|
+
responseHeaders: harHeadersToRecord(res?.headers),
|
|
1692
|
+
responseBody: content?.text ?? null,
|
|
1693
|
+
responseTimeMs: typeof entry.time === "number" ? entry.time : 0,
|
|
1694
|
+
isBinary,
|
|
1695
|
+
error: null
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function parseHar(content, config) {
|
|
1699
|
+
let parsed;
|
|
1700
|
+
try {
|
|
1701
|
+
parsed = JSON.parse(content);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return [];
|
|
1704
|
+
}
|
|
1705
|
+
const entries = parsed.log?.entries ?? [];
|
|
1706
|
+
const records = [];
|
|
1707
|
+
entries.forEach((entry, i) => {
|
|
1708
|
+
const rec = harEntryToRecord(entry, i);
|
|
1709
|
+
if (!rec) return;
|
|
1710
|
+
if (!matchesUrlFilter(rec.url, config.includeUrls, config.excludeUrls)) return;
|
|
1711
|
+
records.push(redactApiCall(rec, config.redactHeaders, config.redactBodyFields));
|
|
1712
|
+
});
|
|
1713
|
+
return records;
|
|
1714
|
+
}
|
|
1715
|
+
function parseHarFile(filePath, config) {
|
|
1716
|
+
try {
|
|
1717
|
+
return parseHar((0, import_node_fs11.readFileSync)(filePath, "utf-8"), config);
|
|
1718
|
+
} catch {
|
|
1719
|
+
return [];
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function bindApiCallsToFlows(calls, flows) {
|
|
1723
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1724
|
+
for (const f of flows) buckets.set(f.testId, []);
|
|
1725
|
+
if (calls.length === 0 || flows.length === 0) return buckets;
|
|
1726
|
+
const windows = flows.map((f) => ({
|
|
1727
|
+
testId: f.testId,
|
|
1728
|
+
startMs: Date.parse(f.startedAt),
|
|
1729
|
+
endMs: Date.parse(f.completedAt)
|
|
1730
|
+
}));
|
|
1731
|
+
for (const call of calls) {
|
|
1732
|
+
const t = Date.parse(call.timestamp);
|
|
1733
|
+
if (!Number.isFinite(t)) continue;
|
|
1734
|
+
const hit = windows.find((w) => Number.isFinite(w.startMs) && Number.isFinite(w.endMs) && t >= w.startMs && t <= w.endMs);
|
|
1735
|
+
if (hit) {
|
|
1736
|
+
buckets.get(hit.testId).push(call);
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
let best = null;
|
|
1740
|
+
for (const w of windows) {
|
|
1741
|
+
if (!Number.isFinite(w.startMs) || !Number.isFinite(w.endMs)) continue;
|
|
1742
|
+
const dist = t < w.startMs ? w.startMs - t : t - w.endMs;
|
|
1743
|
+
if (!best || dist < best.dist) best = { testId: w.testId, dist };
|
|
1744
|
+
}
|
|
1745
|
+
if (best) buckets.get(best.testId).push(call);
|
|
1746
|
+
}
|
|
1747
|
+
return buckets;
|
|
1748
|
+
}
|
|
1749
|
+
function collectNetworkByFlow(jsonlDir, config, flows) {
|
|
1750
|
+
if (!config.enabled && !config.harPath) return /* @__PURE__ */ new Map();
|
|
1751
|
+
const all = [];
|
|
1752
|
+
if (jsonlDir) {
|
|
1753
|
+
for (const file of discoverNetworkJsonlFiles(jsonlDir)) {
|
|
1754
|
+
all.push(...parseNetworkJsonlFile(file, config));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (config.harPath && (0, import_node_fs11.existsSync)(config.harPath)) {
|
|
1758
|
+
all.push(...parseHarFile(config.harPath, config));
|
|
1759
|
+
}
|
|
1760
|
+
if (all.length === 0) return /* @__PURE__ */ new Map();
|
|
1761
|
+
return bindApiCallsToFlows(all, flows);
|
|
1762
|
+
}
|
|
1763
|
+
function reindexFlowCalls(calls) {
|
|
1764
|
+
return calls.map((c, i) => ({ ...c, id: `api-call-${i}` }));
|
|
1765
|
+
}
|
|
1766
|
+
|
|
957
1767
|
// src/artifact-collector.ts
|
|
958
|
-
var
|
|
959
|
-
var
|
|
1768
|
+
var import_node_fs12 = require("fs");
|
|
1769
|
+
var import_node_path10 = require("path");
|
|
960
1770
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
961
1771
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov"]);
|
|
962
1772
|
function walkDir(dir) {
|
|
963
1773
|
const results = [];
|
|
964
|
-
if (!(0,
|
|
1774
|
+
if (!(0, import_node_fs12.existsSync)(dir)) return results;
|
|
965
1775
|
function recurse(current) {
|
|
966
1776
|
try {
|
|
967
|
-
const entries = (0,
|
|
1777
|
+
const entries = (0, import_node_fs12.readdirSync)(current, { withFileTypes: true });
|
|
968
1778
|
for (const entry of entries) {
|
|
969
|
-
const fullPath = (0,
|
|
1779
|
+
const fullPath = (0, import_node_path10.join)(current, entry.name);
|
|
970
1780
|
if (entry.isDirectory()) {
|
|
971
1781
|
recurse(fullPath);
|
|
972
1782
|
} else if (entry.isFile()) {
|
|
@@ -993,8 +1803,8 @@ function collectArtifacts(testOutputDir, debugOutputDir) {
|
|
|
993
1803
|
for (const filePath of allFiles) {
|
|
994
1804
|
if (seen.has(filePath)) continue;
|
|
995
1805
|
seen.add(filePath);
|
|
996
|
-
const name = (0,
|
|
997
|
-
const ext = (0,
|
|
1806
|
+
const name = (0, import_node_path10.basename)(filePath).toLowerCase();
|
|
1807
|
+
const ext = (0, import_node_path10.extname)(filePath).toLowerCase();
|
|
998
1808
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
999
1809
|
screenshotPaths.push(filePath);
|
|
1000
1810
|
continue;
|
|
@@ -1055,24 +1865,106 @@ function mapCommandCategory(category) {
|
|
|
1055
1865
|
return "custom_step";
|
|
1056
1866
|
}
|
|
1057
1867
|
}
|
|
1058
|
-
function
|
|
1868
|
+
function buildStepTitle(cmd) {
|
|
1869
|
+
const d = cmd.details;
|
|
1870
|
+
if (d) {
|
|
1871
|
+
switch (cmd.command) {
|
|
1872
|
+
case "inputText":
|
|
1873
|
+
case "pasteText":
|
|
1874
|
+
if (typeof d.text === "string") return `${cmd.command} \u2192 ${d.text}`;
|
|
1875
|
+
break;
|
|
1876
|
+
case "swipe":
|
|
1877
|
+
case "scroll":
|
|
1878
|
+
case "scrollUntilVisible":
|
|
1879
|
+
if (typeof d.direction === "string") return `${cmd.command} ${d.direction}`;
|
|
1880
|
+
break;
|
|
1881
|
+
case "setLocation":
|
|
1882
|
+
if (typeof d.latitude === "number" && typeof d.longitude === "number") {
|
|
1883
|
+
return `setLocation ${d.latitude},${d.longitude}`;
|
|
1884
|
+
}
|
|
1885
|
+
break;
|
|
1886
|
+
case "pressKey":
|
|
1887
|
+
if (typeof d.key === "string") return `pressKey ${d.key}`;
|
|
1888
|
+
break;
|
|
1889
|
+
case "setOrientation":
|
|
1890
|
+
if (typeof d.orientation === "string") return `setOrientation ${d.orientation}`;
|
|
1891
|
+
break;
|
|
1892
|
+
case "launchApp":
|
|
1893
|
+
case "killApp":
|
|
1894
|
+
case "stopApp":
|
|
1895
|
+
case "clearState":
|
|
1896
|
+
if (typeof d.appId === "string") return `${cmd.command} ${d.appId}`;
|
|
1897
|
+
break;
|
|
1898
|
+
case "openLink":
|
|
1899
|
+
if (typeof d.url === "string") return `openLink ${d.url}`;
|
|
1900
|
+
break;
|
|
1901
|
+
case "runScript":
|
|
1902
|
+
case "evalScript":
|
|
1903
|
+
if (typeof d.path === "string") return `${cmd.command} ${d.path}`;
|
|
1904
|
+
if (typeof d.sourceDescription === "string") return `${cmd.command} ${d.sourceDescription}`;
|
|
1905
|
+
break;
|
|
1906
|
+
case "assertWithAI":
|
|
1907
|
+
case "assertNoDefectsWithAI":
|
|
1908
|
+
case "extractTextWithAI":
|
|
1909
|
+
if (typeof d.prompt === "string") return `${cmd.command} \u2192 ${d.prompt}`;
|
|
1910
|
+
break;
|
|
1911
|
+
case "retry":
|
|
1912
|
+
if (typeof d.maxRetries === "number") return `retry \xD7 ${d.maxRetries}`;
|
|
1913
|
+
break;
|
|
1914
|
+
case "repeat":
|
|
1915
|
+
if (typeof d.times === "number") return `repeat \xD7 ${d.times}`;
|
|
1916
|
+
break;
|
|
1917
|
+
case "runFlow":
|
|
1918
|
+
if (typeof d.file === "string") return `runFlow ${d.file}`;
|
|
1919
|
+
break;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return cmd.selector ? `${cmd.command} \u2192 ${cmd.selector}` : cmd.command;
|
|
1923
|
+
}
|
|
1924
|
+
function commandToAction(cmd, flowStartedMs) {
|
|
1925
|
+
const cmdMs = Date.parse(cmd.timestamp);
|
|
1926
|
+
const videoOffset = Number.isFinite(cmdMs) && Number.isFinite(flowStartedMs) ? Math.max(0, (cmdMs - flowStartedMs) / 1e3) : null;
|
|
1927
|
+
const children = (cmd.children ?? []).map((c) => commandToAction(c, flowStartedMs));
|
|
1059
1928
|
return {
|
|
1060
|
-
title: cmd
|
|
1929
|
+
title: buildStepTitle(cmd),
|
|
1061
1930
|
category: mapCommandCategory(cmd.category),
|
|
1062
1931
|
status: cmd.status === "failed" ? "failed" : "passed",
|
|
1063
1932
|
duration: cmd.duration,
|
|
1064
1933
|
timestamp: cmd.timestamp,
|
|
1065
|
-
videoOffset
|
|
1934
|
+
videoOffset,
|
|
1066
1935
|
error: cmd.error ?? null,
|
|
1067
|
-
children
|
|
1936
|
+
children
|
|
1068
1937
|
};
|
|
1069
1938
|
}
|
|
1070
|
-
function
|
|
1939
|
+
function sortActions(a, b) {
|
|
1940
|
+
const ta = Date.parse(a.timestamp);
|
|
1941
|
+
const tb = Date.parse(b.timestamp);
|
|
1942
|
+
if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
|
|
1943
|
+
return 0;
|
|
1944
|
+
}
|
|
1945
|
+
function buildTestResult(flow, apiCalls) {
|
|
1946
|
+
const flowStartedMs = Date.parse(flow.startedAt);
|
|
1947
|
+
const orderedCommands = [...flow.commands].sort((a, b) => {
|
|
1948
|
+
const ta = Date.parse(a.timestamp);
|
|
1949
|
+
const tb = Date.parse(b.timestamp);
|
|
1950
|
+
if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
|
|
1951
|
+
const sa = a.sequenceNumber ?? 0;
|
|
1952
|
+
const sb = b.sequenceNumber ?? 0;
|
|
1953
|
+
return sa - sb;
|
|
1954
|
+
});
|
|
1955
|
+
const orderedAssertions = [...flow.assertions].sort((a, b) => {
|
|
1956
|
+
const ta = Date.parse(a.timestamp);
|
|
1957
|
+
const tb = Date.parse(b.timestamp);
|
|
1958
|
+
if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
|
|
1959
|
+
const sa = a.sequenceNumber ?? 0;
|
|
1960
|
+
const sb = b.sequenceNumber ?? 0;
|
|
1961
|
+
return sa - sb;
|
|
1962
|
+
});
|
|
1071
1963
|
const actions = [
|
|
1072
|
-
...
|
|
1073
|
-
...
|
|
1964
|
+
...orderedCommands.map((c) => commandToAction(c, flowStartedMs)),
|
|
1965
|
+
...orderedAssertions.map((c) => commandToAction(c, flowStartedMs))
|
|
1074
1966
|
];
|
|
1075
|
-
actions.sort(
|
|
1967
|
+
actions.sort(sortActions);
|
|
1076
1968
|
const artifacts = flow.screenshotPaths.length > 0 || flow.videoPath ? {
|
|
1077
1969
|
screenshot: flow.screenshotPaths[0] ?? void 0,
|
|
1078
1970
|
video: flow.videoPath ?? void 0
|
|
@@ -1100,14 +1992,16 @@ function buildTestResult(flow) {
|
|
|
1100
1992
|
actualStatus: flow.status,
|
|
1101
1993
|
artifacts,
|
|
1102
1994
|
networkRequests: null,
|
|
1103
|
-
apiCalls: null,
|
|
1995
|
+
apiCalls: apiCalls.length > 0 ? [...apiCalls] : null,
|
|
1104
1996
|
apiAssertions: null,
|
|
1105
1997
|
actions: actions.length > 0 ? actions : null,
|
|
1106
1998
|
consoleLogs: null
|
|
1107
1999
|
};
|
|
1108
2000
|
}
|
|
1109
|
-
function buildTimelineEntry(flow) {
|
|
1110
|
-
const
|
|
2001
|
+
function buildTimelineEntry(flow, apiCallsByFlow) {
|
|
2002
|
+
const testId = `${flow.flowFile}::${flow.flowName}`;
|
|
2003
|
+
const apiCalls = apiCallsByFlow?.get(testId) ?? [];
|
|
2004
|
+
const testResult = buildTestResult(flow, apiCalls);
|
|
1111
2005
|
return {
|
|
1112
2006
|
url: flow.appId ?? "maestro-flow",
|
|
1113
2007
|
navigationType: "page_load",
|
|
@@ -1120,8 +2014,8 @@ function buildTimelineEntry(flow) {
|
|
|
1120
2014
|
tests: [testResult]
|
|
1121
2015
|
};
|
|
1122
2016
|
}
|
|
1123
|
-
function buildTimeline(flows) {
|
|
1124
|
-
return flows.map(buildTimelineEntry);
|
|
2017
|
+
function buildTimeline(flows, apiCallsByFlow) {
|
|
2018
|
+
return flows.map((f) => buildTimelineEntry(f, apiCallsByFlow));
|
|
1125
2019
|
}
|
|
1126
2020
|
|
|
1127
2021
|
// src/summary-builder.ts
|
|
@@ -1132,6 +2026,58 @@ var EMPTY_STATUS_RANGE = {
|
|
|
1132
2026
|
"5xx": 0,
|
|
1133
2027
|
error: 0
|
|
1134
2028
|
};
|
|
2029
|
+
function statusBucket(code) {
|
|
2030
|
+
if (code === null) return "error";
|
|
2031
|
+
if (code >= 200 && code < 300) return "2xx";
|
|
2032
|
+
if (code >= 300 && code < 400) return "3xx";
|
|
2033
|
+
if (code >= 400 && code < 500) return "4xx";
|
|
2034
|
+
if (code >= 500 && code < 600) return "5xx";
|
|
2035
|
+
return "error";
|
|
2036
|
+
}
|
|
2037
|
+
function percentile(sorted, p) {
|
|
2038
|
+
if (sorted.length === 0) return null;
|
|
2039
|
+
const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
|
|
2040
|
+
return sorted[idx];
|
|
2041
|
+
}
|
|
2042
|
+
function buildApiStats(calls) {
|
|
2043
|
+
if (calls.length === 0) {
|
|
2044
|
+
return {
|
|
2045
|
+
totalApiCalls: 0,
|
|
2046
|
+
uniqueApiUrls: 0,
|
|
2047
|
+
apiCallsByMethod: {},
|
|
2048
|
+
apiCallsByStatusRange: { ...EMPTY_STATUS_RANGE },
|
|
2049
|
+
apiResponseTime: null
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
const byMethod = {};
|
|
2053
|
+
const byStatus = { ...EMPTY_STATUS_RANGE };
|
|
2054
|
+
const urls = /* @__PURE__ */ new Set();
|
|
2055
|
+
const times = [];
|
|
2056
|
+
for (const c of calls) {
|
|
2057
|
+
byMethod[c.method] = (byMethod[c.method] ?? 0) + 1;
|
|
2058
|
+
byStatus[statusBucket(c.responseStatusCode)] += 1;
|
|
2059
|
+
urls.add(c.url);
|
|
2060
|
+
if (Number.isFinite(c.responseTimeMs) && c.responseTimeMs > 0) {
|
|
2061
|
+
times.push(c.responseTimeMs);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
times.sort((a, b) => a - b);
|
|
2065
|
+
const apiResponseTime = times.length > 0 ? {
|
|
2066
|
+
p50: percentile(times, 50) ?? 0,
|
|
2067
|
+
p95: percentile(times, 95) ?? 0,
|
|
2068
|
+
p99: percentile(times, 99) ?? 0,
|
|
2069
|
+
avg: times.reduce((s, n) => s + n, 0) / times.length,
|
|
2070
|
+
min: times[0],
|
|
2071
|
+
max: times[times.length - 1]
|
|
2072
|
+
} : null;
|
|
2073
|
+
return {
|
|
2074
|
+
totalApiCalls: calls.length,
|
|
2075
|
+
uniqueApiUrls: urls.size,
|
|
2076
|
+
apiCallsByMethod: byMethod,
|
|
2077
|
+
apiCallsByStatusRange: byStatus,
|
|
2078
|
+
apiResponseTime
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
1135
2081
|
function buildSummary(timeline) {
|
|
1136
2082
|
let total = 0;
|
|
1137
2083
|
let passed = 0;
|
|
@@ -1146,6 +2092,7 @@ function buildSummary(timeline) {
|
|
|
1146
2092
|
let totalActionSteps = 0;
|
|
1147
2093
|
const actionCategoryCounts = {};
|
|
1148
2094
|
const uniqueUrls = /* @__PURE__ */ new Set();
|
|
2095
|
+
const allApiCalls = [];
|
|
1149
2096
|
for (const entry of timeline) {
|
|
1150
2097
|
uniqueUrls.add(entry.url);
|
|
1151
2098
|
for (const test of entry.tests) {
|
|
@@ -1180,8 +2127,12 @@ function buildSummary(timeline) {
|
|
|
1180
2127
|
}
|
|
1181
2128
|
}
|
|
1182
2129
|
}
|
|
2130
|
+
if (test.apiCalls && test.apiCalls.length > 0) {
|
|
2131
|
+
allApiCalls.push(...test.apiCalls);
|
|
2132
|
+
}
|
|
1183
2133
|
}
|
|
1184
2134
|
}
|
|
2135
|
+
const apiStats = buildApiStats(allApiCalls);
|
|
1185
2136
|
return {
|
|
1186
2137
|
total,
|
|
1187
2138
|
passed,
|
|
@@ -1189,11 +2140,7 @@ function buildSummary(timeline) {
|
|
|
1189
2140
|
flaky,
|
|
1190
2141
|
skipped,
|
|
1191
2142
|
timedout,
|
|
1192
|
-
|
|
1193
|
-
uniqueApiUrls: 0,
|
|
1194
|
-
apiCallsByMethod: {},
|
|
1195
|
-
apiCallsByStatusRange: EMPTY_STATUS_RANGE,
|
|
1196
|
-
apiResponseTime: null,
|
|
2143
|
+
...apiStats,
|
|
1197
2144
|
totalAssertions,
|
|
1198
2145
|
passedAssertions,
|
|
1199
2146
|
failedAssertions,
|
|
@@ -1206,8 +2153,8 @@ function buildSummary(timeline) {
|
|
|
1206
2153
|
}
|
|
1207
2154
|
|
|
1208
2155
|
// src/html-report.ts
|
|
1209
|
-
var
|
|
1210
|
-
var
|
|
2156
|
+
var import_node_fs13 = require("fs");
|
|
2157
|
+
var import_node_path11 = require("path");
|
|
1211
2158
|
|
|
1212
2159
|
// src/html-css.ts
|
|
1213
2160
|
var CSS = `
|
|
@@ -2469,13 +3416,13 @@ function renderHtmlDocument(reportJson) {
|
|
|
2469
3416
|
|
|
2470
3417
|
// src/html-report.ts
|
|
2471
3418
|
function generateHtmlReport(report, aiDefects, screenshotPaths, outputPath) {
|
|
2472
|
-
const htmlPath = (0,
|
|
2473
|
-
const htmlDir = (0,
|
|
2474
|
-
if (!(0,
|
|
3419
|
+
const htmlPath = (0, import_node_path11.resolve)(outputPath);
|
|
3420
|
+
const htmlDir = (0, import_node_path11.dirname)(htmlPath);
|
|
3421
|
+
if (!(0, import_node_fs13.existsSync)(htmlDir)) (0, import_node_fs13.mkdirSync)(htmlDir, { recursive: true });
|
|
2475
3422
|
const payload = { report, aiDefects, screenshotPaths };
|
|
2476
3423
|
const reportJson = JSON.stringify(payload);
|
|
2477
3424
|
const html = renderHtmlDocument(reportJson);
|
|
2478
|
-
(0,
|
|
3425
|
+
(0, import_node_fs13.writeFileSync)(htmlPath, html, "utf-8");
|
|
2479
3426
|
}
|
|
2480
3427
|
|
|
2481
3428
|
// src/console-summary.ts
|
|
@@ -2538,7 +3485,7 @@ function printConsoleSummary(summary, outputPath, htmlReportPath, quiet, aiDefec
|
|
|
2538
3485
|
}
|
|
2539
3486
|
|
|
2540
3487
|
// src/browser-open.ts
|
|
2541
|
-
var
|
|
3488
|
+
var import_node_child_process4 = require("child_process");
|
|
2542
3489
|
function openInBrowser(filePath) {
|
|
2543
3490
|
const platform = process.platform;
|
|
2544
3491
|
let command;
|
|
@@ -2549,14 +3496,14 @@ function openInBrowser(filePath) {
|
|
|
2549
3496
|
} else {
|
|
2550
3497
|
command = `xdg-open "${filePath}"`;
|
|
2551
3498
|
}
|
|
2552
|
-
(0,
|
|
3499
|
+
(0, import_node_child_process4.exec)(command, () => {
|
|
2553
3500
|
});
|
|
2554
3501
|
}
|
|
2555
3502
|
|
|
2556
3503
|
// src/cloud-client.ts
|
|
2557
|
-
var
|
|
2558
|
-
var
|
|
2559
|
-
var
|
|
3504
|
+
var import_node_fs16 = require("fs");
|
|
3505
|
+
var import_node_path14 = require("path");
|
|
3506
|
+
var import_node_crypto3 = require("crypto");
|
|
2560
3507
|
|
|
2561
3508
|
// src/cloud-auth.ts
|
|
2562
3509
|
var import_core3 = require("@testrelic/core");
|
|
@@ -2587,7 +3534,7 @@ async function healthCheck(endpoint) {
|
|
|
2587
3534
|
}
|
|
2588
3535
|
}
|
|
2589
3536
|
async function sleep(ms) {
|
|
2590
|
-
return new Promise((
|
|
3537
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
2591
3538
|
}
|
|
2592
3539
|
async function parseCloudError(response) {
|
|
2593
3540
|
try {
|
|
@@ -2737,13 +3684,13 @@ async function resolveRepo(endpoint, accessToken, gitId, displayName, timeout, b
|
|
|
2737
3684
|
}
|
|
2738
3685
|
|
|
2739
3686
|
// src/git-metadata.ts
|
|
2740
|
-
var
|
|
2741
|
-
var
|
|
2742
|
-
var
|
|
3687
|
+
var import_node_child_process5 = require("child_process");
|
|
3688
|
+
var import_node_fs14 = require("fs");
|
|
3689
|
+
var import_node_path12 = require("path");
|
|
2743
3690
|
var GIT_TIMEOUT_MS = 5e3;
|
|
2744
3691
|
function execGit(command, cwd) {
|
|
2745
3692
|
try {
|
|
2746
|
-
const result = (0,
|
|
3693
|
+
const result = (0, import_node_child_process5.execSync)(command, {
|
|
2747
3694
|
cwd,
|
|
2748
3695
|
timeout: GIT_TIMEOUT_MS,
|
|
2749
3696
|
encoding: "utf-8",
|
|
@@ -2788,7 +3735,7 @@ function deriveRepoDisplayName(normalizedUrl) {
|
|
|
2788
3735
|
}
|
|
2789
3736
|
function readPackageJsonName(dirPath) {
|
|
2790
3737
|
try {
|
|
2791
|
-
const raw = (0,
|
|
3738
|
+
const raw = (0, import_node_fs14.readFileSync)((0, import_node_path12.join)(dirPath, "package.json"), "utf-8");
|
|
2792
3739
|
const pkg = JSON.parse(raw);
|
|
2793
3740
|
return typeof pkg.name === "string" && pkg.name.length > 0 ? pkg.name : null;
|
|
2794
3741
|
} catch {
|
|
@@ -2798,25 +3745,25 @@ function readPackageJsonName(dirPath) {
|
|
|
2798
3745
|
function deriveNonGitProjectId(dirPath) {
|
|
2799
3746
|
const pkgName = readPackageJsonName(dirPath);
|
|
2800
3747
|
if (pkgName) return pkgName;
|
|
2801
|
-
return `local/${(0,
|
|
3748
|
+
return `local/${(0, import_node_path12.basename)(dirPath)}`;
|
|
2802
3749
|
}
|
|
2803
3750
|
|
|
2804
3751
|
// src/cloud-queue.ts
|
|
2805
|
-
var
|
|
2806
|
-
var
|
|
3752
|
+
var import_node_fs15 = require("fs");
|
|
3753
|
+
var import_node_path13 = require("path");
|
|
2807
3754
|
var import_core4 = require("@testrelic/core");
|
|
2808
3755
|
var QUEUE_ENTRY_VERSION = "1.0.0";
|
|
2809
3756
|
var FLUSH_BATCH_SIZE = 10;
|
|
2810
3757
|
function writeToQueue(queueDirectory, runId, type, reason, targetEndpoint, method, payload, headers) {
|
|
2811
3758
|
try {
|
|
2812
|
-
(0,
|
|
3759
|
+
(0, import_node_fs15.mkdirSync)(queueDirectory, { recursive: true });
|
|
2813
3760
|
const timestamp = Date.now();
|
|
2814
3761
|
const filename = `${timestamp}-${runId}-${type}.json`;
|
|
2815
|
-
const filePath = (0,
|
|
3762
|
+
const filePath = (0, import_node_path13.join)(queueDirectory, filename);
|
|
2816
3763
|
const entry = { version: QUEUE_ENTRY_VERSION, queuedAt: new Date(timestamp).toISOString(), reason, retryCount: 0, targetEndpoint, method, payload, headers };
|
|
2817
3764
|
const tmpPath = filePath + ".tmp";
|
|
2818
|
-
(0,
|
|
2819
|
-
(0,
|
|
3765
|
+
(0, import_node_fs15.writeFileSync)(tmpPath, JSON.stringify(entry, null, 2), "utf-8");
|
|
3766
|
+
(0, import_node_fs15.renameSync)(tmpPath, filePath);
|
|
2820
3767
|
} catch (err) {
|
|
2821
3768
|
const code = err.code;
|
|
2822
3769
|
if (code === "ENOSPC") {
|
|
@@ -2829,7 +3776,7 @@ function writeToQueue(queueDirectory, runId, type, reason, targetEndpoint, metho
|
|
|
2829
3776
|
async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
2830
3777
|
let files;
|
|
2831
3778
|
try {
|
|
2832
|
-
files = (0,
|
|
3779
|
+
files = (0, import_node_fs15.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp")).sort();
|
|
2833
3780
|
} catch {
|
|
2834
3781
|
return;
|
|
2835
3782
|
}
|
|
@@ -2840,14 +3787,14 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
|
2840
3787
|
}
|
|
2841
3788
|
for (const batch of batches) {
|
|
2842
3789
|
for (const file of batch) {
|
|
2843
|
-
const filePath = (0,
|
|
3790
|
+
const filePath = (0, import_node_path13.join)(queueDirectory, file);
|
|
2844
3791
|
try {
|
|
2845
|
-
const stat = (0,
|
|
3792
|
+
const stat = (0, import_node_fs15.statSync)(filePath);
|
|
2846
3793
|
if (!stat.isFile()) continue;
|
|
2847
|
-
const raw = (0,
|
|
3794
|
+
const raw = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
|
|
2848
3795
|
const entry = JSON.parse(raw);
|
|
2849
3796
|
if (!(0, import_core4.isValidQueueEntry)(entry)) {
|
|
2850
|
-
(0,
|
|
3797
|
+
(0, import_node_fs15.unlinkSync)(filePath);
|
|
2851
3798
|
continue;
|
|
2852
3799
|
}
|
|
2853
3800
|
const queueEntry = entry;
|
|
@@ -2857,7 +3804,7 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
|
2857
3804
|
body: JSON.stringify(queueEntry.payload)
|
|
2858
3805
|
});
|
|
2859
3806
|
if (response.ok) {
|
|
2860
|
-
(0,
|
|
3807
|
+
(0, import_node_fs15.unlinkSync)(filePath);
|
|
2861
3808
|
} else {
|
|
2862
3809
|
return;
|
|
2863
3810
|
}
|
|
@@ -2869,21 +3816,21 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
|
|
|
2869
3816
|
}
|
|
2870
3817
|
function cleanupExpiredQueue(queueDirectory, maxAge) {
|
|
2871
3818
|
try {
|
|
2872
|
-
const files = (0,
|
|
3819
|
+
const files = (0, import_node_fs15.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
|
|
2873
3820
|
const now = Date.now();
|
|
2874
3821
|
for (const file of files) {
|
|
2875
|
-
const filePath = (0,
|
|
3822
|
+
const filePath = (0, import_node_path13.join)(queueDirectory, file);
|
|
2876
3823
|
try {
|
|
2877
|
-
const raw = (0,
|
|
3824
|
+
const raw = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
|
|
2878
3825
|
const entry = JSON.parse(raw);
|
|
2879
3826
|
const queuedAt = entry.queuedAt;
|
|
2880
3827
|
if (typeof queuedAt === "string") {
|
|
2881
3828
|
const queuedTime = new Date(queuedAt).getTime();
|
|
2882
|
-
if (now - queuedTime > maxAge) (0,
|
|
3829
|
+
if (now - queuedTime > maxAge) (0, import_node_fs15.unlinkSync)(filePath);
|
|
2883
3830
|
}
|
|
2884
3831
|
} catch {
|
|
2885
3832
|
try {
|
|
2886
|
-
(0,
|
|
3833
|
+
(0, import_node_fs15.unlinkSync)(filePath);
|
|
2887
3834
|
} catch {
|
|
2888
3835
|
}
|
|
2889
3836
|
}
|
|
@@ -3026,7 +3973,7 @@ var CloudClient = class {
|
|
|
3026
3973
|
}
|
|
3027
3974
|
if (this.flushPromise) {
|
|
3028
3975
|
try {
|
|
3029
|
-
await Promise.race([this.flushPromise, new Promise((
|
|
3976
|
+
await Promise.race([this.flushPromise, new Promise((resolve6) => setTimeout(resolve6, FLUSH_TIMEOUT_MS))]);
|
|
3030
3977
|
} catch {
|
|
3031
3978
|
}
|
|
3032
3979
|
this.flushPromise = null;
|
|
@@ -3084,10 +4031,10 @@ var CloudClient = class {
|
|
|
3084
4031
|
}
|
|
3085
4032
|
readRepoCache(gitId) {
|
|
3086
4033
|
if (!this.config) return null;
|
|
3087
|
-
const cachePath = (0,
|
|
4034
|
+
const cachePath = (0, import_node_path14.join)(this.config.queueDirectory, "..", "cache", "repo.json");
|
|
3088
4035
|
try {
|
|
3089
|
-
if (!(0,
|
|
3090
|
-
const raw = (0,
|
|
4036
|
+
if (!(0, import_node_fs16.existsSync)(cachePath)) return null;
|
|
4037
|
+
const raw = (0, import_node_fs16.readFileSync)(cachePath, "utf-8");
|
|
3091
4038
|
const cache = JSON.parse(raw);
|
|
3092
4039
|
if (cache.gitId !== gitId) return null;
|
|
3093
4040
|
if (Date.now() - cache.resolvedAt > PROJECT_CACHE_TTL_MS) return null;
|
|
@@ -3100,20 +4047,20 @@ var CloudClient = class {
|
|
|
3100
4047
|
}
|
|
3101
4048
|
writeRepoCache(gitId, repoId, displayName) {
|
|
3102
4049
|
if (!this.config) return;
|
|
3103
|
-
const cacheDir = (0,
|
|
3104
|
-
const cachePath = (0,
|
|
4050
|
+
const cacheDir = (0, import_node_path14.join)(this.config.queueDirectory, "..", "cache");
|
|
4051
|
+
const cachePath = (0, import_node_path14.join)(cacheDir, "repo.json");
|
|
3105
4052
|
try {
|
|
3106
|
-
(0,
|
|
4053
|
+
(0, import_node_fs16.mkdirSync)(cacheDir, { recursive: true });
|
|
3107
4054
|
const cache = { repoId, gitId, displayName, resolvedAt: Date.now(), apiKeyHash: this.hashApiKey() ?? "" };
|
|
3108
4055
|
const tmpPath = cachePath + ".tmp";
|
|
3109
|
-
(0,
|
|
3110
|
-
(0,
|
|
4056
|
+
(0, import_node_fs16.writeFileSync)(tmpPath, JSON.stringify(cache, null, 2), "utf-8");
|
|
4057
|
+
(0, import_node_fs16.renameSync)(tmpPath, cachePath);
|
|
3111
4058
|
} catch {
|
|
3112
4059
|
}
|
|
3113
4060
|
}
|
|
3114
4061
|
hashApiKey() {
|
|
3115
4062
|
if (!this.config?.apiKey) return null;
|
|
3116
|
-
return (0,
|
|
4063
|
+
return (0, import_node_crypto3.createHash)("sha256").update(this.config.apiKey).digest("hex").substring(0, 16);
|
|
3117
4064
|
}
|
|
3118
4065
|
startBackgroundFlush() {
|
|
3119
4066
|
if (!this.config || !this.authState.accessToken) return;
|
|
@@ -3122,7 +4069,7 @@ var CloudClient = class {
|
|
|
3122
4069
|
const endpoint = this.config.endpoint;
|
|
3123
4070
|
this.flushPromise = Promise.race([
|
|
3124
4071
|
flushQueue(queueDir, endpoint, accessToken),
|
|
3125
|
-
new Promise((
|
|
4072
|
+
new Promise((resolve6) => setTimeout(resolve6, FLUSH_TIMEOUT_MS))
|
|
3126
4073
|
]).catch(() => {
|
|
3127
4074
|
});
|
|
3128
4075
|
}
|
|
@@ -3179,7 +4126,7 @@ function buildUploadPayload(report, repoGitId, git, ci, tests) {
|
|
|
3179
4126
|
return payload;
|
|
3180
4127
|
}
|
|
3181
4128
|
async function sleep2(ms) {
|
|
3182
|
-
return new Promise((
|
|
4129
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
3183
4130
|
}
|
|
3184
4131
|
async function retryWithBackoff(url, init, onTokenRefresh) {
|
|
3185
4132
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
@@ -3255,8 +4202,8 @@ async function uploadBatchRun(endpoint, accessToken, payload, onTokenRefresh) {
|
|
|
3255
4202
|
}
|
|
3256
4203
|
|
|
3257
4204
|
// src/cloud-artifact-upload.ts
|
|
3258
|
-
var
|
|
3259
|
-
var
|
|
4205
|
+
var import_node_fs17 = require("fs");
|
|
4206
|
+
var import_node_path15 = require("path");
|
|
3260
4207
|
var import_node_stream = require("stream");
|
|
3261
4208
|
var UPLOAD_CONCURRENCY = 5;
|
|
3262
4209
|
var RETRY_DELAYS_MS2 = [1e3, 3e3, 9e3];
|
|
@@ -3275,7 +4222,7 @@ var activeUploads = 0;
|
|
|
3275
4222
|
var pendingUploads = [];
|
|
3276
4223
|
async function acquireSlot() {
|
|
3277
4224
|
if (activeUploads >= UPLOAD_CONCURRENCY) {
|
|
3278
|
-
await new Promise((
|
|
4225
|
+
await new Promise((resolve6) => pendingUploads.push(resolve6));
|
|
3279
4226
|
}
|
|
3280
4227
|
activeUploads++;
|
|
3281
4228
|
}
|
|
@@ -3284,7 +4231,7 @@ function releaseSlot() {
|
|
|
3284
4231
|
if (pendingUploads.length > 0) pendingUploads.shift()();
|
|
3285
4232
|
}
|
|
3286
4233
|
async function sleep3(ms) {
|
|
3287
|
-
return new Promise((
|
|
4234
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
3288
4235
|
}
|
|
3289
4236
|
function getContentType(filePath) {
|
|
3290
4237
|
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
@@ -3292,14 +4239,14 @@ function getContentType(filePath) {
|
|
|
3292
4239
|
}
|
|
3293
4240
|
function getFileSize(filePath) {
|
|
3294
4241
|
try {
|
|
3295
|
-
return (0,
|
|
4242
|
+
return (0, import_node_fs17.statSync)(filePath).size;
|
|
3296
4243
|
} catch {
|
|
3297
4244
|
return 0;
|
|
3298
4245
|
}
|
|
3299
4246
|
}
|
|
3300
4247
|
async function requestUploadUrl(endpoint, accessToken, request, sizeBytes, contentType) {
|
|
3301
4248
|
const url = `${endpoint}/artifacts/upload-url`;
|
|
3302
|
-
const body = JSON.stringify({ runId: request.runId, testId: request.testId, fileName: (0,
|
|
4249
|
+
const body = JSON.stringify({ runId: request.runId, testId: request.testId, fileName: (0, import_node_path15.basename)(request.filePath), contentType, type: request.type, sizeBytes });
|
|
3303
4250
|
for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
|
|
3304
4251
|
try {
|
|
3305
4252
|
const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${accessToken}` }, body });
|
|
@@ -3322,7 +4269,7 @@ async function requestUploadUrl(endpoint, accessToken, request, sizeBytes, conte
|
|
|
3322
4269
|
async function putFileToPresignedUrl(presignedUrl, filePath, contentType, sizeBytes) {
|
|
3323
4270
|
for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
|
|
3324
4271
|
try {
|
|
3325
|
-
const nodeStream = (0,
|
|
4272
|
+
const nodeStream = (0, import_node_fs17.createReadStream)(filePath);
|
|
3326
4273
|
const webStream = import_node_stream.Readable.toWeb(nodeStream);
|
|
3327
4274
|
const response = await fetch(presignedUrl, {
|
|
3328
4275
|
method: "PUT",
|
|
@@ -3615,35 +4562,35 @@ async function finalizeAndUpload(cloudClient, cloudConfig, testRunId, report, co
|
|
|
3615
4562
|
// src/report-orchestrator.ts
|
|
3616
4563
|
function matchVideoToFlow(flowFile, videoPaths, flowRecordingPath) {
|
|
3617
4564
|
if (videoPaths.length === 0) return null;
|
|
3618
|
-
const flowBase = (0,
|
|
4565
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3619
4566
|
const exact = videoPaths.find(
|
|
3620
|
-
(v) => (0,
|
|
4567
|
+
(v) => (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase() === flowBase
|
|
3621
4568
|
);
|
|
3622
4569
|
if (exact) return exact;
|
|
3623
4570
|
const forward = videoPaths.find(
|
|
3624
|
-
(v) => (0,
|
|
4571
|
+
(v) => (0, import_node_path16.basename)(v).toLowerCase().includes(flowBase)
|
|
3625
4572
|
);
|
|
3626
4573
|
if (forward) return forward;
|
|
3627
4574
|
const reverse = videoPaths.find((v) => {
|
|
3628
|
-
const videoBase = (0,
|
|
4575
|
+
const videoBase = (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase();
|
|
3629
4576
|
return videoBase.length >= 3 && (flowBase.startsWith(videoBase) || flowBase.includes(videoBase));
|
|
3630
4577
|
});
|
|
3631
4578
|
if (reverse) return reverse;
|
|
3632
4579
|
if (flowRecordingPath) {
|
|
3633
|
-
const recBase = (0,
|
|
3634
|
-
const byRec = videoPaths.find((v) => (0,
|
|
4580
|
+
const recBase = (0, import_node_path16.basename)(flowRecordingPath, (0, import_node_path16.extname)(flowRecordingPath)).toLowerCase();
|
|
4581
|
+
const byRec = videoPaths.find((v) => (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase() === recBase);
|
|
3635
4582
|
if (byRec) return byRec;
|
|
3636
|
-
const byRecPartial = videoPaths.find((v) => (0,
|
|
4583
|
+
const byRecPartial = videoPaths.find((v) => (0, import_node_path16.basename)(v).toLowerCase().includes(recBase));
|
|
3637
4584
|
if (byRecPartial) return byRecPartial;
|
|
3638
4585
|
}
|
|
3639
4586
|
return null;
|
|
3640
4587
|
}
|
|
3641
4588
|
function matchScreenshotsToFlow(flowFile, screenshotPaths) {
|
|
3642
4589
|
if (screenshotPaths.length === 0) return [];
|
|
3643
|
-
const flowBase = (0,
|
|
4590
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3644
4591
|
const matched = screenshotPaths.filter((s) => {
|
|
3645
|
-
const name = (0,
|
|
3646
|
-
const nameBase = (0,
|
|
4592
|
+
const name = (0, import_node_path16.basename)(s).toLowerCase();
|
|
4593
|
+
const nameBase = (0, import_node_path16.basename)(s, (0, import_node_path16.extname)(s)).toLowerCase();
|
|
3647
4594
|
if (name.startsWith(flowBase) || name.includes(flowBase)) return true;
|
|
3648
4595
|
return nameBase.length >= 3 && (flowBase.startsWith(nameBase) || flowBase.includes(nameBase));
|
|
3649
4596
|
});
|
|
@@ -3660,21 +4607,24 @@ function extractRecordingPath(commands) {
|
|
|
3660
4607
|
}
|
|
3661
4608
|
return null;
|
|
3662
4609
|
}
|
|
4610
|
+
function normaliseCommandFileName(filePath) {
|
|
4611
|
+
return (0, import_node_path16.basename)(filePath).replace(/^commands[-_]?/i, "").replace(/\.json$/i, "").replace(/^\(/, "").replace(/\)$/, "").toLowerCase();
|
|
4612
|
+
}
|
|
3663
4613
|
function buildFlowRecordingMap(commandSteps) {
|
|
3664
4614
|
const map = /* @__PURE__ */ new Map();
|
|
3665
4615
|
for (const [filePath, commands] of commandSteps) {
|
|
3666
|
-
const name = (
|
|
4616
|
+
const name = normaliseCommandFileName(filePath);
|
|
3667
4617
|
const recPath = extractRecordingPath(commands);
|
|
3668
4618
|
if (recPath && name) map.set(name, recPath);
|
|
3669
4619
|
}
|
|
3670
4620
|
return map;
|
|
3671
4621
|
}
|
|
3672
4622
|
function commandsForFlow(commandSteps, flowFile) {
|
|
3673
|
-
const flowBase = (0,
|
|
4623
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3674
4624
|
if (!flowBase || commandSteps.size === 0) {
|
|
3675
4625
|
return { commands: [], matchedAnyFile: false };
|
|
3676
4626
|
}
|
|
3677
|
-
const stripCmdPrefix = (p) => (
|
|
4627
|
+
const stripCmdPrefix = (p) => normaliseCommandFileName(p);
|
|
3678
4628
|
for (const [cmdFile, cmds] of commandSteps) {
|
|
3679
4629
|
if (stripCmdPrefix(cmdFile) === flowBase) {
|
|
3680
4630
|
return { commands: cmds, matchedAnyFile: true };
|
|
@@ -3706,7 +4656,7 @@ function platformToOs(platform) {
|
|
|
3706
4656
|
return void 0;
|
|
3707
4657
|
}
|
|
3708
4658
|
}
|
|
3709
|
-
function flowToTestResult(flow) {
|
|
4659
|
+
function flowToTestResult(flow, apiCalls) {
|
|
3710
4660
|
const consoleLogs = flow.logEntries.length > 0 ? flow.logEntries.map((entry) => ({
|
|
3711
4661
|
level: entry.level.toLowerCase(),
|
|
3712
4662
|
message: entry.message,
|
|
@@ -3733,7 +4683,8 @@ function flowToTestResult(flow) {
|
|
|
3733
4683
|
platform: flow.platform !== "unknown" ? flow.platform : void 0,
|
|
3734
4684
|
os: platformToOs(flow.platform),
|
|
3735
4685
|
deviceName: flow.deviceId,
|
|
3736
|
-
...consoleLogs ? { consoleLogs } : {}
|
|
4686
|
+
...consoleLogs ? { consoleLogs } : {},
|
|
4687
|
+
...apiCalls && apiCalls.length > 0 ? { apiCalls } : {}
|
|
3737
4688
|
};
|
|
3738
4689
|
}
|
|
3739
4690
|
function logEntriesForFlow(allEntries, flowStartedAt, flowCompletedAt) {
|
|
@@ -3752,13 +4703,19 @@ function logEntriesForFlow(allEntries, flowStartedAt, flowCompletedAt) {
|
|
|
3752
4703
|
async function orchestrateReport(input) {
|
|
3753
4704
|
const { config } = input;
|
|
3754
4705
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3755
|
-
const testRunId = config.testRunId ?? (0,
|
|
4706
|
+
const testRunId = config.testRunId ?? (0, import_node_crypto4.randomUUID)();
|
|
3756
4707
|
const artifacts = collectArtifacts(input.testOutputDir, input.debugOutputDir);
|
|
3757
4708
|
const junitPath = input.junitPath ?? artifacts.junitReportPath;
|
|
3758
4709
|
const logFiles = input.debugOutputDir ? discoverLogFiles(input.debugOutputDir) : artifacts.logPaths;
|
|
3759
4710
|
const allLogEntries = [];
|
|
3760
4711
|
for (const logFile of logFiles) {
|
|
3761
|
-
|
|
4712
|
+
let mtimeDate;
|
|
4713
|
+
try {
|
|
4714
|
+
const { statSync: statSync5 } = await import("fs");
|
|
4715
|
+
mtimeDate = statSync5(logFile).mtime;
|
|
4716
|
+
} catch {
|
|
4717
|
+
}
|
|
4718
|
+
const entries = parseLogFile(logFile, mtimeDate);
|
|
3762
4719
|
if (entries.length > 0) allLogEntries.push(...entries);
|
|
3763
4720
|
}
|
|
3764
4721
|
let platform = input.platform ?? "unknown";
|
|
@@ -3767,7 +4724,7 @@ async function orchestrateReport(input) {
|
|
|
3767
4724
|
if (detected !== "unknown") platform = detected;
|
|
3768
4725
|
}
|
|
3769
4726
|
const flowMetadataMap = /* @__PURE__ */ new Map();
|
|
3770
|
-
if (input.flowsDir && (0,
|
|
4727
|
+
if (input.flowsDir && (0, import_node_fs18.existsSync)(input.flowsDir)) {
|
|
3771
4728
|
const flowFiles = discoverFlowFiles(input.flowsDir);
|
|
3772
4729
|
for (const flowFile of flowFiles) {
|
|
3773
4730
|
const meta = parseFlowFile(flowFile);
|
|
@@ -3788,7 +4745,7 @@ async function orchestrateReport(input) {
|
|
|
3788
4745
|
}
|
|
3789
4746
|
const flowRecordingMap = buildFlowRecordingMap(commandSteps);
|
|
3790
4747
|
const flowResults = [];
|
|
3791
|
-
if (junitPath && (0,
|
|
4748
|
+
if (junitPath && (0, import_node_fs18.existsSync)(junitPath)) {
|
|
3792
4749
|
const junit = parseJUnitFile(junitPath);
|
|
3793
4750
|
const allCommands = Array.from(commandSteps.values()).flat();
|
|
3794
4751
|
const allAssertions = allCommands.filter((c) => c.category === "assertion");
|
|
@@ -3799,14 +4756,19 @@ async function orchestrateReport(input) {
|
|
|
3799
4756
|
const meta = flowMetadataMap.get(name);
|
|
3800
4757
|
const status = testCase.status === "SUCCESS" ? "passed" : testCase.status === "SKIPPED" ? "skipped" : "failed";
|
|
3801
4758
|
const durationMs = testCase.time * 1e3;
|
|
3802
|
-
const flowStartedAt = startedAt;
|
|
3803
|
-
const flowCompletedAt = new Date(new Date(flowStartedAt).getTime() + durationMs).toISOString();
|
|
3804
4759
|
const flowFile = testCase.classname || name;
|
|
3805
|
-
const flowBase = (0,
|
|
4760
|
+
const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
|
|
3806
4761
|
const recordingPath = flowRecordingMap.get(flowBase) ?? null;
|
|
3807
4762
|
const { commands: flowCmds, matchedAnyFile } = commandsForFlow(commandSteps, flowFile);
|
|
3808
4763
|
const perFlowCommands = matchedAnyFile ? flowCmds.filter((c) => c.category !== "assertion") : allNonAssertions;
|
|
3809
4764
|
const perFlowAssertions = matchedAnyFile ? flowCmds.filter((c) => c.category === "assertion") : allAssertions;
|
|
4765
|
+
const allFlowCmds = [...perFlowCommands, ...perFlowAssertions];
|
|
4766
|
+
const earliestCmdMs = allFlowCmds.reduce((min, c) => {
|
|
4767
|
+
const t = Date.parse(c.timestamp);
|
|
4768
|
+
return Number.isFinite(t) && t < min ? t : min;
|
|
4769
|
+
}, Number.POSITIVE_INFINITY);
|
|
4770
|
+
const flowStartedAt = Number.isFinite(earliestCmdMs) ? new Date(earliestCmdMs).toISOString() : startedAt;
|
|
4771
|
+
const flowCompletedAt = new Date(new Date(flowStartedAt).getTime() + durationMs).toISOString();
|
|
3810
4772
|
flowResults.push({
|
|
3811
4773
|
flowName: name,
|
|
3812
4774
|
flowFile,
|
|
@@ -3863,7 +4825,20 @@ async function orchestrateReport(input) {
|
|
|
3863
4825
|
});
|
|
3864
4826
|
}
|
|
3865
4827
|
}
|
|
3866
|
-
const
|
|
4828
|
+
const apiCallsByFlow = (() => {
|
|
4829
|
+
if (!config.network?.enabled && !config.network?.harPath) return void 0;
|
|
4830
|
+
const windows = flowResults.map((f) => ({
|
|
4831
|
+
testId: `${f.flowFile}::${f.flowName}`,
|
|
4832
|
+
startedAt: f.startedAt,
|
|
4833
|
+
completedAt: f.completedAt
|
|
4834
|
+
}));
|
|
4835
|
+
const bound = collectNetworkByFlow(input.networkJsonlDir ?? null, config.network, windows);
|
|
4836
|
+
if (bound.size === 0) return void 0;
|
|
4837
|
+
const remapped = /* @__PURE__ */ new Map();
|
|
4838
|
+
for (const [k, v] of bound) remapped.set(k, reindexFlowCalls(v));
|
|
4839
|
+
return remapped;
|
|
4840
|
+
})();
|
|
4841
|
+
const timeline = buildTimeline(flowResults, apiCallsByFlow);
|
|
3867
4842
|
const summary = buildSummary(timeline);
|
|
3868
4843
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3869
4844
|
const totalDuration = Date.now() - new Date(startedAt).getTime();
|
|
@@ -3879,12 +4854,12 @@ async function orchestrateReport(input) {
|
|
|
3879
4854
|
timeline,
|
|
3880
4855
|
shardRunIds: null
|
|
3881
4856
|
};
|
|
3882
|
-
(0,
|
|
3883
|
-
(0,
|
|
3884
|
-
generateHtmlReport(report, allAiDefects, artifacts.screenshotPaths, (0,
|
|
4857
|
+
(0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)((0, import_node_path16.resolve)(config.outputPath)), { recursive: true });
|
|
4858
|
+
(0, import_node_fs18.writeFileSync)((0, import_node_path16.resolve)(config.outputPath), JSON.stringify(report, null, 2), "utf-8");
|
|
4859
|
+
generateHtmlReport(report, allAiDefects, artifacts.screenshotPaths, (0, import_node_path16.resolve)(config.htmlReportPath));
|
|
3885
4860
|
printConsoleSummary(summary, config.outputPath, config.htmlReportPath, config.quiet, allAiDefects);
|
|
3886
4861
|
if (config.openReport) {
|
|
3887
|
-
openInBrowser((0,
|
|
4862
|
+
openInBrowser((0, import_node_path16.resolve)(config.htmlReportPath));
|
|
3888
4863
|
}
|
|
3889
4864
|
if (config.cloud) {
|
|
3890
4865
|
const cloudClient = new CloudClient(config.cloud);
|
|
@@ -3893,7 +4868,7 @@ async function orchestrateReport(input) {
|
|
|
3893
4868
|
const videoArtifacts = [];
|
|
3894
4869
|
if (videoPathsToUpload.length > 0) {
|
|
3895
4870
|
for (const flow of flowResults) {
|
|
3896
|
-
const flowBase = (0,
|
|
4871
|
+
const flowBase = (0, import_node_path16.basename)(flow.flowFile, (0, import_node_path16.extname)(flow.flowFile)).toLowerCase();
|
|
3897
4872
|
const recordingPath = flowRecordingMap.get(flowBase) ?? null;
|
|
3898
4873
|
const matched = matchVideoToFlow(flow.flowFile, videoPathsToUpload, recordingPath);
|
|
3899
4874
|
if (matched) {
|
|
@@ -3915,7 +4890,10 @@ async function orchestrateReport(input) {
|
|
|
3915
4890
|
}
|
|
3916
4891
|
const matchedPaths = new Set(videoArtifacts.map((va) => va.path));
|
|
3917
4892
|
const unmatchedVideos = videoPathsToUpload.filter((v) => !matchedPaths.has(v));
|
|
3918
|
-
const testsForUpload = flowResults.map(
|
|
4893
|
+
const testsForUpload = flowResults.map((f) => {
|
|
4894
|
+
const testId = `${f.flowFile}::${f.flowName}`;
|
|
4895
|
+
return flowToTestResult(f, apiCallsByFlow?.get(testId));
|
|
4896
|
+
});
|
|
3919
4897
|
await finalizeAndUpload(
|
|
3920
4898
|
cloudClient,
|
|
3921
4899
|
config.cloud,
|
|
@@ -3962,6 +4940,16 @@ Test Options:
|
|
|
3962
4940
|
-e, --env <KEY=VALUE> Environment variable
|
|
3963
4941
|
--quiet Suppress output
|
|
3964
4942
|
|
|
4943
|
+
Network Capture Options (mobile API logs):
|
|
4944
|
+
--capture-network Spawn mitmproxy to capture HTTP traffic from the app
|
|
4945
|
+
(requires mitmdump on PATH; --har-path is an alternative)
|
|
4946
|
+
--proxy-port <n> Proxy listen port (default: 8080)
|
|
4947
|
+
--proxy-host <host> Proxy listen host (default: 127.0.0.1)
|
|
4948
|
+
--har-path <file> Import a pre-recorded HAR file instead of running a proxy
|
|
4949
|
+
--skip-cert-install Skip auto-installing the mitmproxy CA on the device
|
|
4950
|
+
--proxy-include <pat> Only capture URLs matching this substring or regex (repeatable)
|
|
4951
|
+
--proxy-exclude <pat> Drop URLs matching this substring or regex (repeatable)
|
|
4952
|
+
|
|
3965
4953
|
Report Options:
|
|
3966
4954
|
--junit <path> Path to JUnit XML report
|
|
3967
4955
|
--artifacts <dir> Path to Maestro test output directory
|
|
@@ -4001,6 +4989,15 @@ function parseArgs(args) {
|
|
|
4001
4989
|
} else {
|
|
4002
4990
|
options["env"] = args[i];
|
|
4003
4991
|
}
|
|
4992
|
+
} else if (key === "proxy-include" || key === "proxy-exclude") {
|
|
4993
|
+
const existing = options[key];
|
|
4994
|
+
if (Array.isArray(existing)) {
|
|
4995
|
+
existing.push(args[i]);
|
|
4996
|
+
} else if (typeof existing === "string") {
|
|
4997
|
+
options[key] = [existing, args[i]];
|
|
4998
|
+
} else {
|
|
4999
|
+
options[key] = args[i];
|
|
5000
|
+
}
|
|
4004
5001
|
} else {
|
|
4005
5002
|
options[key] = args[i];
|
|
4006
5003
|
}
|
|
@@ -4036,11 +5033,30 @@ function parseEnvVars(raw) {
|
|
|
4036
5033
|
}
|
|
4037
5034
|
return env;
|
|
4038
5035
|
}
|
|
5036
|
+
function asStringArray(v) {
|
|
5037
|
+
if (!v) return [];
|
|
5038
|
+
return Array.isArray(v) ? v : [v];
|
|
5039
|
+
}
|
|
5040
|
+
function buildNetworkOptions(options) {
|
|
5041
|
+
const enabled = options["capture-network"] === "true";
|
|
5042
|
+
const harPath = typeof options["har-path"] === "string" ? options["har-path"] : void 0;
|
|
5043
|
+
if (!enabled && !harPath) return void 0;
|
|
5044
|
+
return {
|
|
5045
|
+
enabled,
|
|
5046
|
+
proxyPort: typeof options["proxy-port"] === "string" ? Number.parseInt(options["proxy-port"], 10) : void 0,
|
|
5047
|
+
proxyHost: typeof options["proxy-host"] === "string" ? options["proxy-host"] : void 0,
|
|
5048
|
+
harPath,
|
|
5049
|
+
skipCertInstall: options["skip-cert-install"] === "true",
|
|
5050
|
+
includeUrls: asStringArray(options["proxy-include"]),
|
|
5051
|
+
excludeUrls: asStringArray(options["proxy-exclude"])
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
4039
5054
|
async function handleTest(options, positional) {
|
|
4040
5055
|
if (positional.length === 0) {
|
|
4041
5056
|
process.stderr.write("Error: No flow paths specified. Usage: testrelic-maestro test <flow-paths...>\n");
|
|
4042
5057
|
process.exit(1);
|
|
4043
5058
|
}
|
|
5059
|
+
const network = buildNetworkOptions(options);
|
|
4044
5060
|
const testOptions = {
|
|
4045
5061
|
flowPaths: positional,
|
|
4046
5062
|
apiKey: typeof options["api-key"] === "string" ? options["api-key"] : void 0,
|
|
@@ -4054,12 +5070,10 @@ async function handleTest(options, positional) {
|
|
|
4054
5070
|
shards: typeof options["shards"] === "string" ? parseInt(options["shards"], 10) : void 0,
|
|
4055
5071
|
config: typeof options["config"] === "string" ? options["config"] : void 0,
|
|
4056
5072
|
env: parseEnvVars(options["env"]),
|
|
4057
|
-
quiet: options["quiet"] === "true"
|
|
5073
|
+
quiet: options["quiet"] === "true",
|
|
5074
|
+
network
|
|
4058
5075
|
};
|
|
4059
5076
|
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
5077
|
const config = resolveConfig({
|
|
4064
5078
|
outputPath: typeof options["output-dir"] === "string" ? `${options["output-dir"]}/testrelic-maestro.json` : void 0,
|
|
4065
5079
|
htmlReportPath: typeof options["html-report"] === "string" ? options["html-report"] : void 0,
|
|
@@ -4069,18 +5083,26 @@ async function handleTest(options, positional) {
|
|
|
4069
5083
|
endpoint: typeof options["endpoint"] === "string" ? options["endpoint"] : void 0
|
|
4070
5084
|
},
|
|
4071
5085
|
quiet: options["quiet"] === "true",
|
|
4072
|
-
flowsDir: positional[0]
|
|
5086
|
+
flowsDir: positional[0],
|
|
5087
|
+
network
|
|
4073
5088
|
});
|
|
5089
|
+
const runResult = await runMaestro(testOptions, { network: config.network });
|
|
5090
|
+
process.stderr.write(`\u2139 TestRelic: Maestro exited with code ${runResult.exitCode}. Parsing results...
|
|
5091
|
+
`);
|
|
4074
5092
|
await orchestrateReport({
|
|
4075
5093
|
junitPath: runResult.junitPath,
|
|
4076
5094
|
testOutputDir: runResult.testOutputDir,
|
|
4077
5095
|
debugOutputDir: runResult.debugOutputDir,
|
|
4078
5096
|
flowsDir: positional[0],
|
|
4079
|
-
config
|
|
5097
|
+
config,
|
|
5098
|
+
platform: testOptions.platform ?? "unknown",
|
|
5099
|
+
device: testOptions.device,
|
|
5100
|
+
networkJsonlDir: runResult.networkJsonlDir
|
|
4080
5101
|
});
|
|
4081
5102
|
process.exit(runResult.exitCode);
|
|
4082
5103
|
}
|
|
4083
5104
|
async function handleReport(options) {
|
|
5105
|
+
const network = buildNetworkOptions(options);
|
|
4084
5106
|
const config = resolveConfig({
|
|
4085
5107
|
outputPath: typeof options["output-dir"] === "string" ? `${options["output-dir"]}/testrelic-maestro.json` : void 0,
|
|
4086
5108
|
htmlReportPath: typeof options["html-report"] === "string" ? options["html-report"] : void 0,
|
|
@@ -4090,14 +5112,16 @@ async function handleReport(options) {
|
|
|
4090
5112
|
endpoint: typeof options["endpoint"] === "string" ? options["endpoint"] : void 0
|
|
4091
5113
|
},
|
|
4092
5114
|
quiet: options["quiet"] === "true",
|
|
4093
|
-
flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0
|
|
5115
|
+
flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0,
|
|
5116
|
+
network
|
|
4094
5117
|
});
|
|
4095
5118
|
process.stderr.write("\u2139 TestRelic: Parsing Maestro artifacts...\n");
|
|
4096
5119
|
await orchestrateReport({
|
|
4097
5120
|
junitPath: typeof options["junit"] === "string" ? options["junit"] : void 0,
|
|
4098
5121
|
testOutputDir: typeof options["artifacts"] === "string" ? options["artifacts"] : void 0,
|
|
4099
5122
|
flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0,
|
|
4100
|
-
config
|
|
5123
|
+
config,
|
|
5124
|
+
networkJsonlDir: typeof options["network-dir"] === "string" ? options["network-dir"] : null
|
|
4101
5125
|
});
|
|
4102
5126
|
}
|
|
4103
5127
|
async function main() {
|