doc-detective 4.41.6 → 4.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/bin/doc-detective-lsp.js +2 -0
- package/dist/agents/adapters/claude-code.js +1 -1
- package/dist/agents/adapters/claude-code.js.map +1 -1
- package/dist/agents/adapters/copilot-cli.js +2 -2
- package/dist/agents/adapters/copilot-cli.js.map +1 -1
- package/dist/agents/adapters/gemini-cli.js +1 -1
- package/dist/agents/adapters/gemini-cli.js.map +1 -1
- package/dist/agents/adapters/qwen-code.js +1 -1
- package/dist/agents/adapters/qwen-code.js.map +1 -1
- package/dist/cli.js +27 -6
- package/dist/cli.js.map +1 -1
- package/dist/common/src/schemas/schemas.json +2 -2
- package/dist/common/src/schemas/schemasReferenced.json +448 -294
- package/dist/common/src/schemas/validators.cjs +140 -124
- package/dist/common/src/schemas/validatorsCheck.cjs +1 -1
- package/dist/common/src/types/generated/annotate_v3.d.ts +12 -12
- package/dist/common/src/types/generated/annotate_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/annotation_v3.d.ts +6 -6
- package/dist/common/src/types/generated/annotation_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/checkLink_v3.d.ts +4 -0
- package/dist/common/src/types/generated/checkLink_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/click_v3.d.ts +2 -2
- package/dist/common/src/types/generated/click_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/config_v3.d.ts +260 -108
- package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/context_v3.d.ts +10 -2
- package/dist/common/src/types/generated/context_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +2 -2
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/find_v3.d.ts +6 -6
- package/dist/common/src/types/generated/find_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/goTo_v3.d.ts +1 -1
- package/dist/common/src/types/generated/goTo_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/httpRequest_v3.d.ts +2 -2
- package/dist/common/src/types/generated/httpRequest_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/record_v3.d.ts +5 -5
- package/dist/common/src/types/generated/record_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/report_v3.d.ts +529 -207
- package/dist/common/src/types/generated/report_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts +754 -314
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/runCode_v3.d.ts +11 -2
- package/dist/common/src/types/generated/runCode_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/runShell_v3.d.ts +11 -2
- package/dist/common/src/types/generated/runShell_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/screenshot_v3.d.ts +12 -12
- package/dist/common/src/types/generated/screenshot_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/spec_v3.d.ts +494 -206
- package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/startSurface_v3.d.ts +24 -6
- package/dist/common/src/types/generated/startSurface_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/step_v3.d.ts +223 -100
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/test_v3.d.ts +482 -202
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/type_v3.d.ts +3 -3
- package/dist/common/src/types/generated/type_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/waitUntil_v3.d.ts +2 -2
- package/dist/common/src/types/generated/waitUntil_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/wait_v3.d.ts +2 -3
- package/dist/common/src/types/generated/wait_v3.d.ts.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +6 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/files.d.ts +18 -3
- package/dist/core/files.d.ts.map +1 -1
- package/dist/core/files.js +58 -18
- package/dist/core/files.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +16 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/integrations/index.d.ts +2 -1
- package/dist/core/integrations/index.d.ts.map +1 -1
- package/dist/core/integrations/index.js +15 -5
- package/dist/core/integrations/index.js.map +1 -1
- package/dist/core/openapi.d.ts.map +1 -1
- package/dist/core/openapi.js +122 -6
- package/dist/core/openapi.js.map +1 -1
- package/dist/core/ptyWatchdog.d.ts.map +1 -1
- package/dist/core/ptyWatchdog.js +6 -3
- package/dist/core/ptyWatchdog.js.map +1 -1
- package/dist/core/resolveTests.d.ts.map +1 -1
- package/dist/core/resolveTests.js +5 -3
- package/dist/core/resolveTests.js.map +1 -1
- package/dist/core/telem.d.ts.map +1 -1
- package/dist/core/telem.js +5 -7
- package/dist/core/telem.js.map +1 -1
- package/dist/core/tests/checkLink.d.ts.map +1 -1
- package/dist/core/tests/checkLink.js +12 -7
- package/dist/core/tests/checkLink.js.map +1 -1
- package/dist/core/tests/click.d.ts.map +1 -1
- package/dist/core/tests/click.js +7 -0
- package/dist/core/tests/click.js.map +1 -1
- package/dist/core/tests/findElement.d.ts.map +1 -1
- package/dist/core/tests/findElement.js +3 -2
- package/dist/core/tests/findElement.js.map +1 -1
- package/dist/core/tests/goTo.d.ts.map +1 -1
- package/dist/core/tests/goTo.js +2 -1
- package/dist/core/tests/goTo.js.map +1 -1
- package/dist/core/tests/httpRequest.d.ts.map +1 -1
- package/dist/core/tests/httpRequest.js +4 -5
- package/dist/core/tests/httpRequest.js.map +1 -1
- package/dist/core/tests/processSurface.d.ts +4 -1
- package/dist/core/tests/processSurface.d.ts.map +1 -1
- package/dist/core/tests/processSurface.js +3 -0
- package/dist/core/tests/processSurface.js.map +1 -1
- package/dist/core/tests/saveScreenshot.d.ts.map +1 -1
- package/dist/core/tests/saveScreenshot.js +8 -3
- package/dist/core/tests/saveScreenshot.js.map +1 -1
- package/dist/core/tests/startRecording.d.ts.map +1 -1
- package/dist/core/tests/startRecording.js +2 -1
- package/dist/core/tests/startRecording.js.map +1 -1
- package/dist/core/tests/stopRecording.d.ts.map +1 -1
- package/dist/core/tests/stopRecording.js +2 -1
- package/dist/core/tests/stopRecording.js.map +1 -1
- package/dist/core/tests/timing.d.ts +4 -0
- package/dist/core/tests/timing.d.ts.map +1 -0
- package/dist/core/tests/timing.js +8 -0
- package/dist/core/tests/timing.js.map +1 -0
- package/dist/core/tests.d.ts +15 -0
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +77 -30
- package/dist/core/tests.js.map +1 -1
- package/dist/core/utils.d.ts +42 -2
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +281 -89
- package/dist/core/utils.js.map +1 -1
- package/dist/debug/index.d.ts.map +1 -1
- package/dist/debug/index.js +8 -36
- package/dist/debug/index.js.map +1 -1
- package/dist/hints/hints.js +2 -2
- package/dist/hints/hints.js.map +1 -1
- package/dist/index.cjs +684 -235
- package/dist/reporters/htmlReporter.js +1 -1
- package/dist/runtime/cacheDir.d.ts.map +1 -1
- package/dist/runtime/cacheDir.js +6 -11
- package/dist/runtime/cacheDir.js.map +1 -1
- package/dist/runtime/heavyDeps.d.ts +12 -0
- package/dist/runtime/heavyDeps.d.ts.map +1 -1
- package/dist/runtime/heavyDeps.js +31 -7
- package/dist/runtime/heavyDeps.js.map +1 -1
- package/dist/runtime/selfUpdate.d.ts +3 -3
- package/dist/runtime/selfUpdate.d.ts.map +1 -1
- package/dist/runtime/selfUpdate.js +34 -5
- package/dist/runtime/selfUpdate.js.map +1 -1
- package/dist/upgradeNotice.d.ts +41 -0
- package/dist/upgradeNotice.d.ts.map +1 -0
- package/dist/upgradeNotice.js +356 -0
- package/dist/upgradeNotice.js.map +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +17 -8
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
- package/scripts/postinstall.js +1 -1
package/dist/index.cjs
CHANGED
|
@@ -518,12 +518,27 @@ function resolveManagedDepNames(pkg) {
|
|
|
518
518
|
]);
|
|
519
519
|
return withPeerCompanions([...names]);
|
|
520
520
|
}
|
|
521
|
+
function packageRoot() {
|
|
522
|
+
for (let dir = __dirname; !cachedRoot; dir = import_node_path.default.dirname(dir)) {
|
|
523
|
+
try {
|
|
524
|
+
const pkg = JSON.parse(import_node_fs.default.readFileSync(import_node_path.default.join(dir, "package.json"), "utf8"));
|
|
525
|
+
if (pkg.name === "doc-detective") {
|
|
526
|
+
cachedRoot = dir;
|
|
527
|
+
cachedPkg = pkg;
|
|
528
|
+
}
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
if (import_node_path.default.dirname(dir) === dir)
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
return cachedRoot;
|
|
535
|
+
}
|
|
536
|
+
function ownVersion() {
|
|
537
|
+
return packageRoot() && cachedPkg.version;
|
|
538
|
+
}
|
|
521
539
|
function readShimPackageJson() {
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
const pkgPath = import_node_path.default.resolve(__dirname, "../../package.json");
|
|
525
|
-
const raw = import_node_fs.default.readFileSync(pkgPath, "utf8");
|
|
526
|
-
cachedPkg = JSON.parse(raw);
|
|
540
|
+
if (!packageRoot())
|
|
541
|
+
throw new Error("doc-detective's package.json not found");
|
|
527
542
|
return cachedPkg;
|
|
528
543
|
}
|
|
529
544
|
function getDeclaredVersion(name) {
|
|
@@ -583,7 +598,7 @@ function compareTuple(a, b) {
|
|
|
583
598
|
}
|
|
584
599
|
return 0;
|
|
585
600
|
}
|
|
586
|
-
var import_node_fs, import_node_path, import_node_url, __dirname, HEAVY_NPM_DEPS, RUNTIME_PEER_COMPANIONS, cachedPkg;
|
|
601
|
+
var import_node_fs, import_node_path, import_node_url, __dirname, HEAVY_NPM_DEPS, RUNTIME_PEER_COMPANIONS, cachedRoot, cachedPkg;
|
|
587
602
|
var init_heavyDeps = __esm({
|
|
588
603
|
"dist/runtime/heavyDeps.js"() {
|
|
589
604
|
"use strict";
|
|
@@ -675,9 +690,9 @@ function getBrowsersDir(ctx = {}) {
|
|
|
675
690
|
}
|
|
676
691
|
function legacyBrowserSnapshotCandidates() {
|
|
677
692
|
const out = [import_node_path2.default.resolve("browser-snapshots")];
|
|
678
|
-
const shimRoot =
|
|
679
|
-
const fromShim = import_node_path2.default.join(shimRoot, "browser-snapshots");
|
|
680
|
-
if (fromShim !== out[0])
|
|
693
|
+
const shimRoot = packageRoot();
|
|
694
|
+
const fromShim = shimRoot && import_node_path2.default.join(shimRoot, "browser-snapshots");
|
|
695
|
+
if (fromShim && fromShim !== out[0])
|
|
681
696
|
out.push(fromShim);
|
|
682
697
|
return out;
|
|
683
698
|
}
|
|
@@ -734,15 +749,14 @@ function writeInstalledRecord(record, ctx = {}) {
|
|
|
734
749
|
}
|
|
735
750
|
}
|
|
736
751
|
}
|
|
737
|
-
var import_node_fs2, import_node_os, import_node_path2,
|
|
752
|
+
var import_node_fs2, import_node_os, import_node_path2, SHELL_METAS;
|
|
738
753
|
var init_cacheDir = __esm({
|
|
739
754
|
"dist/runtime/cacheDir.js"() {
|
|
740
755
|
"use strict";
|
|
741
756
|
import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
742
757
|
import_node_os = __toESM(require("node:os"), 1);
|
|
743
758
|
import_node_path2 = __toESM(require("node:path"), 1);
|
|
744
|
-
|
|
745
|
-
__dirname2 = import_node_path2.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
759
|
+
init_heavyDeps();
|
|
746
760
|
SHELL_METAS = /[;&|`$<>"'\n\r%^!]/;
|
|
747
761
|
}
|
|
748
762
|
});
|
|
@@ -810,7 +824,7 @@ function tryResolveFromCache(name, ctx = {}) {
|
|
|
810
824
|
const pkgJsonAnchor = import_node_path3.default.join(runtimeDir, "package.json");
|
|
811
825
|
if (!import_node_fs3.default.existsSync(pkgJsonAnchor))
|
|
812
826
|
return null;
|
|
813
|
-
const requireFromCache = (0, import_node_module.createRequire)((0,
|
|
827
|
+
const requireFromCache = (0, import_node_module.createRequire)((0, import_node_url2.pathToFileURL)(pkgJsonAnchor).href);
|
|
814
828
|
return resolveEntry(requireFromCache, name);
|
|
815
829
|
} catch (err) {
|
|
816
830
|
const detail = err instanceof Error ? err.stack ?? `${err.name}: ${err.message}` : (0, import_node_util.inspect)(err);
|
|
@@ -870,7 +884,7 @@ async function loadHeavyDep(name, options = {}) {
|
|
|
870
884
|
throw new Error(`Failed to resolve heavy dep '${name}' even after install. Inspect <cacheDir>/runtime/node_modules/${name}.`);
|
|
871
885
|
}
|
|
872
886
|
}
|
|
873
|
-
return await import((0,
|
|
887
|
+
return await import((0, import_node_url2.pathToFileURL)(resolved).href);
|
|
874
888
|
}
|
|
875
889
|
function heavyDepPrefetchTarget(name, ctx = {}) {
|
|
876
890
|
const entry = resolveHeavyDepPath(name, ctx);
|
|
@@ -883,7 +897,7 @@ function prefetchHeavyDep(name, ctx = {}) {
|
|
|
883
897
|
try {
|
|
884
898
|
const target = heavyDepPrefetchTarget(name, ctx);
|
|
885
899
|
if (target)
|
|
886
|
-
import((0,
|
|
900
|
+
import((0, import_node_url2.pathToFileURL)(target).href).catch(() => {
|
|
887
901
|
});
|
|
888
902
|
} catch {
|
|
889
903
|
}
|
|
@@ -1099,14 +1113,14 @@ async function ensureRuntimeInstalled(packages, options = {}) {
|
|
|
1099
1113
|
}
|
|
1100
1114
|
recordRuntimeDependencies(runtimeDir, ctx);
|
|
1101
1115
|
}
|
|
1102
|
-
var import_node_path3, import_node_fs3, import_node_child_process,
|
|
1116
|
+
var import_node_path3, import_node_fs3, import_node_child_process, import_node_url2, import_node_module, import_node_util, RUNTIME_DEBUG, defaultLogger, requireFromShim, PREFETCH_SIBLING, DEFAULT_INSTALL_TIMEOUT_MS, RUNTIME_PACKAGE_JSON_CONTENTS;
|
|
1103
1117
|
var init_loader = __esm({
|
|
1104
1118
|
"dist/runtime/loader.js"() {
|
|
1105
1119
|
"use strict";
|
|
1106
1120
|
import_node_path3 = __toESM(require("node:path"), 1);
|
|
1107
1121
|
import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
1108
1122
|
import_node_child_process = require("node:child_process");
|
|
1109
|
-
|
|
1123
|
+
import_node_url2 = require("node:url");
|
|
1110
1124
|
import_node_module = require("node:module");
|
|
1111
1125
|
import_node_util = require("node:util");
|
|
1112
1126
|
init_heavyDeps();
|
|
@@ -1532,8 +1546,8 @@ var init_windowsBash = __esm({
|
|
|
1532
1546
|
|
|
1533
1547
|
// dist/core/ptyWatchdog.js
|
|
1534
1548
|
function defaultCreateWorker(data) {
|
|
1535
|
-
const
|
|
1536
|
-
return new import_node_worker_threads.Worker(
|
|
1549
|
+
const worker = import_node_path5.default.join(packageRoot(), "dist", "core", "ptyProbeWorker.js");
|
|
1550
|
+
return new import_node_worker_threads.Worker(worker, { workerData: data });
|
|
1537
1551
|
}
|
|
1538
1552
|
function probePtyAllocation(opts) {
|
|
1539
1553
|
const timeoutMs = opts.timeoutMs ?? PTY_PROBE_TIMEOUT_MS;
|
|
@@ -1615,12 +1629,14 @@ async function assertConptyAllocatable(opts) {
|
|
|
1615
1629
|
throw err;
|
|
1616
1630
|
}
|
|
1617
1631
|
}
|
|
1618
|
-
var import_node_fs6, import_node_worker_threads, PTY_PROBE_TIMEOUT_MS;
|
|
1632
|
+
var import_node_fs6, import_node_path5, import_node_worker_threads, PTY_PROBE_TIMEOUT_MS;
|
|
1619
1633
|
var init_ptyWatchdog = __esm({
|
|
1620
1634
|
"dist/core/ptyWatchdog.js"() {
|
|
1621
1635
|
"use strict";
|
|
1622
1636
|
import_node_fs6 = __toESM(require("node:fs"), 1);
|
|
1637
|
+
import_node_path5 = __toESM(require("node:path"), 1);
|
|
1623
1638
|
import_node_worker_threads = require("node:worker_threads");
|
|
1639
|
+
init_heavyDeps();
|
|
1624
1640
|
PTY_PROBE_TIMEOUT_MS = Number(process.env.DOC_DETECTIVE_PTY_PROBE_TIMEOUT_MS) || 15e3;
|
|
1625
1641
|
}
|
|
1626
1642
|
});
|
|
@@ -2388,6 +2404,24 @@ var init_verification = __esm({
|
|
|
2388
2404
|
function loadAxios() {
|
|
2389
2405
|
return axiosModule ??= import("axios").then((m) => m.default);
|
|
2390
2406
|
}
|
|
2407
|
+
function sessionVersions({ capabilities, app }) {
|
|
2408
|
+
const result = {};
|
|
2409
|
+
const str = (value) => typeof value === "string" && value ? value : void 0;
|
|
2410
|
+
const caps = capabilities ?? {};
|
|
2411
|
+
const browserVersion = str(caps.browserVersion) ?? str(app?.version)?.replace(/^[a-z]+_/, "");
|
|
2412
|
+
if (browserVersion)
|
|
2413
|
+
result.browserVersion = browserVersion;
|
|
2414
|
+
const chromedriver = str(caps.chrome?.chromedriverVersion);
|
|
2415
|
+
if (chromedriver) {
|
|
2416
|
+
result.driver = { name: "chromedriver", version: chromedriver.split(" ")[0] };
|
|
2417
|
+
} else if (app?.name === "firefox") {
|
|
2418
|
+
const version = str(caps["moz:geckodriverVersion"]) ?? /geckodriver-(\d+(?:\.\d+)*)/i.exec(import_node_path6.default.basename(str(app.driver) ?? ""))?.[1];
|
|
2419
|
+
result.driver = { name: "geckodriver", ...version && { version } };
|
|
2420
|
+
} else if (app?.name === "safari") {
|
|
2421
|
+
result.driver = { name: "safaridriver" };
|
|
2422
|
+
}
|
|
2423
|
+
return result;
|
|
2424
|
+
}
|
|
2391
2425
|
function createAppiumPool(ports) {
|
|
2392
2426
|
const available = [...ports];
|
|
2393
2427
|
const waiters = [];
|
|
@@ -2603,11 +2637,9 @@ async function spawnPtyBackgroundCommand(cmd, args = [], options = {}) {
|
|
|
2603
2637
|
try {
|
|
2604
2638
|
ptyProcess = pty.spawn(shell, shellArgs, {
|
|
2605
2639
|
name: "xterm-color",
|
|
2606
|
-
//
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
cols: 120,
|
|
2610
|
-
rows: 30,
|
|
2640
|
+
// `tty: {cols, rows}`; Ajv skips defaults inside the schema's anyOf.
|
|
2641
|
+
cols: options.cols ?? 120,
|
|
2642
|
+
rows: options.rows ?? 30,
|
|
2611
2643
|
cwd: options.cwd || process.cwd(),
|
|
2612
2644
|
env: shellSpawnEnv(options.shell) ?? process.env
|
|
2613
2645
|
});
|
|
@@ -2969,13 +3001,13 @@ function appendQueryParams(url, params) {
|
|
|
2969
3001
|
return pathAndAuthority + (query ? "?" + query : "") + fragment;
|
|
2970
3002
|
}
|
|
2971
3003
|
function cleanTemp() {
|
|
2972
|
-
const tempDir =
|
|
3004
|
+
const tempDir = import_node_path6.default.join(import_node_os2.default.tmpdir(), "doc-detective");
|
|
2973
3005
|
if (!import_node_fs7.default.existsSync(tempDir))
|
|
2974
3006
|
return;
|
|
2975
3007
|
for (const entry of import_node_fs7.default.readdirSync(tempDir)) {
|
|
2976
3008
|
if (PRESERVED_TEMP_ENTRIES.has(entry))
|
|
2977
3009
|
continue;
|
|
2978
|
-
const curPath =
|
|
3010
|
+
const curPath = import_node_path6.default.join(tempDir, entry);
|
|
2979
3011
|
try {
|
|
2980
3012
|
const stat = import_node_fs7.default.statSync(curPath);
|
|
2981
3013
|
if (stat.isDirectory()) {
|
|
@@ -3003,7 +3035,7 @@ function safeFilenameFromUrl(fileURL, fallback) {
|
|
|
3003
3035
|
raw = fileURL;
|
|
3004
3036
|
}
|
|
3005
3037
|
raw = raw.split("?")[0].split("#")[0];
|
|
3006
|
-
const base =
|
|
3038
|
+
const base = import_node_path6.default.basename(raw.replace(/\\/g, "/"));
|
|
3007
3039
|
return sanitizeFilesystemName(base, fallback);
|
|
3008
3040
|
}
|
|
3009
3041
|
function redactUrlForOutput(value) {
|
|
@@ -3041,36 +3073,56 @@ function isPrivateOrLoopbackAddress(ip) {
|
|
|
3041
3073
|
return false;
|
|
3042
3074
|
}
|
|
3043
3075
|
if (import_node_net.default.isIPv6(ip)) {
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
if (
|
|
3076
|
+
const g = ipv6Groups(ip);
|
|
3077
|
+
const zero = (from, to) => g.slice(from, to).every((x) => x === 0);
|
|
3078
|
+
const v4 = `${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`;
|
|
3079
|
+
if ((g[0] & 65024) === 64512)
|
|
3048
3080
|
return true;
|
|
3049
|
-
if (
|
|
3081
|
+
if ((g[0] & 65472) === 65152)
|
|
3050
3082
|
return true;
|
|
3051
|
-
if (
|
|
3083
|
+
if ((g[0] & 65472) === 65216)
|
|
3052
3084
|
return true;
|
|
3053
|
-
if (
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
}
|
|
3065
|
-
return false;
|
|
3085
|
+
if (g[0] === 8194) {
|
|
3086
|
+
return isPrivateOrLoopbackAddress(`${g[1] >> 8}.${g[1] & 255}.${g[2] >> 8}.${g[2] & 255}`);
|
|
3087
|
+
}
|
|
3088
|
+
if (zero(0, 4) && g[4] === 65535 && g[5] === 0)
|
|
3089
|
+
return isPrivateOrLoopbackAddress(v4);
|
|
3090
|
+
if (zero(0, 6))
|
|
3091
|
+
return isPrivateOrLoopbackAddress(v4);
|
|
3092
|
+
if (zero(0, 5) && g[5] === 65535)
|
|
3093
|
+
return isPrivateOrLoopbackAddress(v4);
|
|
3094
|
+
if (g[0] === 100 && g[1] === 65435 && zero(2, 6)) {
|
|
3095
|
+
return isPrivateOrLoopbackAddress(v4);
|
|
3066
3096
|
}
|
|
3067
3097
|
return false;
|
|
3068
3098
|
}
|
|
3069
3099
|
return false;
|
|
3070
3100
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3101
|
+
function ipv6Groups(ip) {
|
|
3102
|
+
let text = ip.toLowerCase().split("%")[0];
|
|
3103
|
+
const dotted = text.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
3104
|
+
if (dotted) {
|
|
3105
|
+
const [a, b, c, d] = dotted.slice(1).map(Number);
|
|
3106
|
+
text = text.slice(0, dotted.index) + `${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
3107
|
+
}
|
|
3108
|
+
const [head, tail] = text.split("::");
|
|
3109
|
+
const left = head ? head.split(":") : [];
|
|
3110
|
+
const right = tail ? tail.split(":") : [];
|
|
3111
|
+
const groups = tail === void 0 ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
3112
|
+
return groups.map((group) => parseInt(group, 16));
|
|
3113
|
+
}
|
|
3114
|
+
function envFlag(name) {
|
|
3115
|
+
return /^(1|true|yes|on)$/i.test((process.env[name] ?? "").trim());
|
|
3116
|
+
}
|
|
3117
|
+
function localUrlPolicy() {
|
|
3118
|
+
const allow = envFlag("DOC_DETECTIVE_ALLOW_LOCAL_URLS");
|
|
3119
|
+
const block = envFlag("DOC_DETECTIVE_BLOCK_LOCAL_URLS");
|
|
3120
|
+
if (allow && block) {
|
|
3121
|
+
throw new UrlGuardError("DOC_DETECTIVE_ALLOW_LOCAL_URLS and DOC_DETECTIVE_BLOCK_LOCAL_URLS can't both be set.");
|
|
3122
|
+
}
|
|
3123
|
+
return allow ? "allow" : block ? "block" : "hops";
|
|
3124
|
+
}
|
|
3125
|
+
async function privateAddressOf(fileURL, { every = false } = {}) {
|
|
3074
3126
|
let parsed;
|
|
3075
3127
|
try {
|
|
3076
3128
|
parsed = new URL(fileURL);
|
|
@@ -3082,33 +3134,117 @@ async function assertUrlHostIsPublic(fileURL) {
|
|
|
3082
3134
|
}
|
|
3083
3135
|
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
3084
3136
|
if (import_node_net.default.isIP(host)) {
|
|
3085
|
-
|
|
3086
|
-
throw new Error(`Refusing to fetch private/loopback address (${host}). Set DOC_DETECTIVE_ALLOW_LOCAL_URLS=true to allow.`);
|
|
3087
|
-
}
|
|
3088
|
-
return;
|
|
3137
|
+
return isPrivateOrLoopbackAddress(host) ? host : void 0;
|
|
3089
3138
|
}
|
|
3090
3139
|
const lower = host.toLowerCase();
|
|
3091
3140
|
if (lower === "localhost" || lower.endsWith(".localhost")) {
|
|
3092
|
-
|
|
3141
|
+
return host;
|
|
3093
3142
|
}
|
|
3094
3143
|
let addresses;
|
|
3095
3144
|
try {
|
|
3096
3145
|
addresses = await import_promises.default.lookup(host, { all: true });
|
|
3097
3146
|
} catch (error) {
|
|
3098
|
-
throw new Error(`Couldn't resolve host ${host}
|
|
3147
|
+
throw Object.assign(new Error(`Couldn't resolve host ${host}: ${error.message}`), { host, code: error.code ?? error.message });
|
|
3099
3148
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3149
|
+
const privates = addresses.filter(({ address }) => isPrivateOrLoopbackAddress(address));
|
|
3150
|
+
const isPrivate = every ? privates.length === addresses.length : privates.length > 0;
|
|
3151
|
+
return isPrivate ? privates[0]?.address : void 0;
|
|
3152
|
+
}
|
|
3153
|
+
function strictModeError(url) {
|
|
3154
|
+
return new UrlGuardError(`Couldn't fetch ${redactUrlForOutput(url)}: DOC_DETECTIVE_BLOCK_LOCAL_URLS refuses private and loopback addresses.
|
|
3155
|
+
Hint: Unset DOC_DETECTIVE_BLOCK_LOCAL_URLS to allow local URLs.`);
|
|
3156
|
+
}
|
|
3157
|
+
function redirectError(from, address) {
|
|
3158
|
+
return new UrlGuardError(`Couldn't follow the redirect from ${redactUrlForOutput(from)}: it leads to ${address}, a private address, from a public one.
|
|
3159
|
+
Hint: If you trust this redirect, set DOC_DETECTIVE_ALLOW_LOCAL_URLS=1.`);
|
|
3160
|
+
}
|
|
3161
|
+
function hostKey(hostname) {
|
|
3162
|
+
return hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
3163
|
+
}
|
|
3164
|
+
function proxyConfigured() {
|
|
3165
|
+
return ["http_proxy", "https_proxy", "all_proxy"].some((key) => process.env[key] || process.env[key.toUpperCase()]);
|
|
3166
|
+
}
|
|
3167
|
+
async function getGuarded(url, opts = FETCH_BINARY_DEFAULTS, refuseFirst, refuseRedirect = redirectError) {
|
|
3168
|
+
const policy = localUrlPolicy();
|
|
3169
|
+
const axios = await loadAxios();
|
|
3170
|
+
const [{ default: http3 }, { default: https4 }] = await Promise.all([
|
|
3171
|
+
import("node:http"),
|
|
3172
|
+
import("node:https")
|
|
3173
|
+
]);
|
|
3174
|
+
let current = url;
|
|
3175
|
+
let refuse = policy === "hops" ? refuseFirst : void 0;
|
|
3176
|
+
let reachedPublic = false;
|
|
3177
|
+
for (let hops = 0; ; hops++) {
|
|
3178
|
+
if (policy === "block") {
|
|
3179
|
+
if (await privateAddressOf(current))
|
|
3180
|
+
throw strictModeError(current);
|
|
3181
|
+
const hopUrl = current;
|
|
3182
|
+
refuse = () => strictModeError(hopUrl);
|
|
3183
|
+
}
|
|
3184
|
+
const seen = {};
|
|
3185
|
+
const refuseAtConnect = refuse;
|
|
3186
|
+
const target = hostKey(new URL(current).hostname);
|
|
3187
|
+
const lookup = (hostname, options, callback) => {
|
|
3188
|
+
if (proxyConfigured() && hostKey(hostname) !== target) {
|
|
3189
|
+
return import_node_dns.default.lookup(hostname, options, callback);
|
|
3190
|
+
}
|
|
3191
|
+
return import_node_dns.default.lookup(hostname, options, (error, address, family) => {
|
|
3192
|
+
if (error)
|
|
3193
|
+
return callback(error, address, family);
|
|
3194
|
+
const answers = Array.isArray(address) ? address : [{ address }];
|
|
3195
|
+
seen.resolved = true;
|
|
3196
|
+
seen.private = answers.find((a) => isPrivateOrLoopbackAddress(a.address))?.address;
|
|
3197
|
+
seen.public = answers.some((a) => !isPrivateOrLoopbackAddress(a.address));
|
|
3198
|
+
if (seen.private && refuseAtConnect)
|
|
3199
|
+
return callback(refuseAtConnect(seen.private));
|
|
3200
|
+
callback(null, address, family);
|
|
3201
|
+
});
|
|
3202
|
+
};
|
|
3203
|
+
const response = await axios.get(current, {
|
|
3204
|
+
...opts,
|
|
3205
|
+
maxRedirects: 0,
|
|
3206
|
+
httpAgent: new http3.Agent({ lookup }),
|
|
3207
|
+
httpsAgent: new https4.Agent({ lookup }),
|
|
3208
|
+
validateStatus: (status) => status >= 200 && status < 300 || REDIRECT_STATUSES.includes(status)
|
|
3209
|
+
}).catch((error) => {
|
|
3210
|
+
throw error?.cause instanceof UrlGuardError ? error.cause : error;
|
|
3211
|
+
});
|
|
3212
|
+
let hopPublic = seen.resolved ? seen.public : import_node_net.default.isIP(target) ? !isPrivateOrLoopbackAddress(target) : void 0;
|
|
3213
|
+
let fresh;
|
|
3214
|
+
const freshPublic = () => fresh ??= privateAddressOf(current, { every: true }).then((a) => !a, () => true);
|
|
3215
|
+
if (hopPublic === void 0 && policy !== "allow")
|
|
3216
|
+
hopPublic = await freshPublic();
|
|
3217
|
+
if (hopPublic)
|
|
3218
|
+
reachedPublic = true;
|
|
3219
|
+
if (!REDIRECT_STATUSES.includes(response.status)) {
|
|
3220
|
+
return { data: response.data, url: current, reachedPublic };
|
|
3221
|
+
}
|
|
3222
|
+
const location = response.headers?.location;
|
|
3223
|
+
if (!location || hops >= 5) {
|
|
3224
|
+
throw new UrlGuardError(`Couldn't fetch ${redactUrlForOutput(url)}: ${hops >= 5 ? "it needs more than 5 redirects" : `the ${response.status} redirect has no Location header`}.`);
|
|
3225
|
+
}
|
|
3226
|
+
const next = new URL(location, current).toString();
|
|
3227
|
+
const scheme = new URL(next).protocol;
|
|
3228
|
+
if (scheme !== "http:" && scheme !== "https:") {
|
|
3229
|
+
throw new UrlGuardError(`Couldn't fetch ${redactUrlForOutput(current)}: the redirect leads to an unsupported scheme (\`${scheme.slice(0, -1)}\`).`);
|
|
3230
|
+
}
|
|
3231
|
+
if (policy === "hops") {
|
|
3232
|
+
const fromPublic = hopPublic || await freshPublic();
|
|
3233
|
+
refuse = void 0;
|
|
3234
|
+
if (fromPublic) {
|
|
3235
|
+
const address = await privateAddressOf(next);
|
|
3236
|
+
if (address)
|
|
3237
|
+
throw refuseRedirect(current, address);
|
|
3238
|
+
const from = current;
|
|
3239
|
+
refuse = (connectAddress) => refuseRedirect(from, connectAddress);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
current = next;
|
|
3104
3243
|
}
|
|
3105
3244
|
}
|
|
3106
3245
|
async function fetchFile(fileURL, opts = {}) {
|
|
3107
3246
|
try {
|
|
3108
|
-
|
|
3109
|
-
await assertUrlHostIsPublic(fileURL);
|
|
3110
|
-
}
|
|
3111
|
-
const response = await (await loadAxios()).get(fileURL, opts.binary ? FETCH_BINARY_DEFAULTS : void 0);
|
|
3247
|
+
const response = opts.binary ? await getGuarded(fileURL) : await (await loadAxios()).get(fileURL);
|
|
3112
3248
|
let data;
|
|
3113
3249
|
if (opts.binary) {
|
|
3114
3250
|
data = Buffer.from(response.data);
|
|
@@ -3119,11 +3255,11 @@ async function fetchFile(fileURL, opts = {}) {
|
|
|
3119
3255
|
}
|
|
3120
3256
|
const fileName = safeFilenameFromUrl(fileURL, "fetched_file");
|
|
3121
3257
|
const hash = import_node_crypto3.default.createHash("md5").update(data).digest("hex");
|
|
3122
|
-
const ddTempDir =
|
|
3123
|
-
const filePath =
|
|
3124
|
-
const resolvedDir =
|
|
3125
|
-
const resolvedFile =
|
|
3126
|
-
if (!resolvedFile.startsWith(resolvedDir +
|
|
3258
|
+
const ddTempDir = import_node_path6.default.join(import_node_os2.default.tmpdir(), "doc-detective");
|
|
3259
|
+
const filePath = import_node_path6.default.join(ddTempDir, `${hash}_${fileName}`);
|
|
3260
|
+
const resolvedDir = import_node_path6.default.resolve(ddTempDir);
|
|
3261
|
+
const resolvedFile = import_node_path6.default.resolve(filePath);
|
|
3262
|
+
if (!resolvedFile.startsWith(resolvedDir + import_node_path6.default.sep)) {
|
|
3127
3263
|
return {
|
|
3128
3264
|
result: "error",
|
|
3129
3265
|
message: new Error(`Refusing to write outside temp dir: ${resolvedFile}`)
|
|
@@ -3302,10 +3438,10 @@ function expandRequirementPath(entry, env) {
|
|
|
3302
3438
|
function commandOnPath(command, env, existsSync) {
|
|
3303
3439
|
const hasSeparator = command.includes("/") || command.includes("\\");
|
|
3304
3440
|
const pathValue = env.PATH ?? env.Path ?? "";
|
|
3305
|
-
const directories = hasSeparator ? [""] : pathValue.split(
|
|
3441
|
+
const directories = hasSeparator ? [""] : pathValue.split(import_node_path6.default.delimiter).filter(Boolean);
|
|
3306
3442
|
const extensions = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
3307
3443
|
for (const directory of directories) {
|
|
3308
|
-
const base = hasSeparator ? command :
|
|
3444
|
+
const base = hasSeparator ? command : import_node_path6.default.join(directory, command);
|
|
3309
3445
|
for (const extension of extensions) {
|
|
3310
3446
|
if (existsSync(base + extension))
|
|
3311
3447
|
return true;
|
|
@@ -3396,7 +3532,7 @@ function shieldsBadge(format, date, { label = "Last verified", color = "brightgr
|
|
|
3396
3532
|
}
|
|
3397
3533
|
function writeFileAtomic(file, content) {
|
|
3398
3534
|
const target = import_node_fs7.default.realpathSync(file);
|
|
3399
|
-
const tmp =
|
|
3535
|
+
const tmp = import_node_path6.default.join(import_node_path6.default.dirname(target), `.${import_node_path6.default.basename(target)}.${import_node_crypto3.default.randomBytes(6).toString("hex")}.tmp`);
|
|
3400
3536
|
const fd = import_node_fs7.default.openSync(tmp, "wx", 384);
|
|
3401
3537
|
let keepTmp = false;
|
|
3402
3538
|
try {
|
|
@@ -3462,7 +3598,7 @@ function applyVerifiedMarkers({ config, results, files: detectedFiles = [], init
|
|
|
3462
3598
|
}
|
|
3463
3599
|
}
|
|
3464
3600
|
const today = verifiedDate();
|
|
3465
|
-
const version = (
|
|
3601
|
+
const version = ownVersion() ?? "unknown";
|
|
3466
3602
|
for (const file of files) {
|
|
3467
3603
|
try {
|
|
3468
3604
|
if (!import_node_fs7.default.existsSync(file))
|
|
@@ -3481,7 +3617,7 @@ function applyVerifiedMarkers({ config, results, files: detectedFiles = [], init
|
|
|
3481
3617
|
version,
|
|
3482
3618
|
resolve: (check) => resolveVerifiedId(results, check),
|
|
3483
3619
|
onUnknown: (check) => log(config, "warning", `verified-by check '${check}' in ${file} matches no test or spec; skipping.`),
|
|
3484
|
-
onSkipped: (line,
|
|
3620
|
+
onSkipped: (line, reason2) => log(config, "warning", `Skipping the meta statement at ${file}:${line}: ${reason2}.`)
|
|
3485
3621
|
});
|
|
3486
3622
|
if (content !== original)
|
|
3487
3623
|
writeFileAtomic(file, content);
|
|
@@ -3641,11 +3777,11 @@ function getRunOutputDir(config, { create = true } = {}) {
|
|
|
3641
3777
|
let base = String(config?.output || ".");
|
|
3642
3778
|
const reportFileExtensions = [".json", ".html", ".htm", ".xml", ".md"];
|
|
3643
3779
|
if (reportFileExtensions.some((ext) => base.toLowerCase().endsWith(ext))) {
|
|
3644
|
-
base =
|
|
3780
|
+
base = import_node_path6.default.dirname(base);
|
|
3645
3781
|
}
|
|
3646
|
-
const runsRoot =
|
|
3782
|
+
const runsRoot = import_node_path6.default.resolve(base, ".doc-detective", "runs");
|
|
3647
3783
|
const runId = getOrInitRunTimestamp(config);
|
|
3648
|
-
let dir =
|
|
3784
|
+
let dir = import_node_path6.default.join(runsRoot, runId);
|
|
3649
3785
|
if (create) {
|
|
3650
3786
|
import_node_fs7.default.mkdirSync(runsRoot, { recursive: true });
|
|
3651
3787
|
let suffix = 2;
|
|
@@ -3656,7 +3792,7 @@ function getRunOutputDir(config, { create = true } = {}) {
|
|
|
3656
3792
|
} catch (error) {
|
|
3657
3793
|
if (error?.code !== "EEXIST")
|
|
3658
3794
|
throw error;
|
|
3659
|
-
dir =
|
|
3795
|
+
dir = import_node_path6.default.join(runsRoot, `${runId}-${suffix++}`);
|
|
3660
3796
|
}
|
|
3661
3797
|
}
|
|
3662
3798
|
}
|
|
@@ -3733,15 +3869,15 @@ function shellSpawnEnv(shellExecutable, deps = {}) {
|
|
|
3733
3869
|
return void 0;
|
|
3734
3870
|
if (!/bash(\.exe)?$/i.test(shellExecutable))
|
|
3735
3871
|
return void 0;
|
|
3736
|
-
const dir =
|
|
3737
|
-
if (!
|
|
3872
|
+
const dir = import_node_path6.default.win32.dirname(shellExecutable);
|
|
3873
|
+
if (!import_node_path6.default.win32.isAbsolute(dir))
|
|
3738
3874
|
return void 0;
|
|
3739
3875
|
const pathKey = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "Path";
|
|
3740
3876
|
const existing = env[pathKey];
|
|
3741
3877
|
return {
|
|
3742
3878
|
...env,
|
|
3743
3879
|
// No trailing delimiter (an empty PATH segment) when PATH was unset.
|
|
3744
|
-
[pathKey]: existing ? `${dir}${
|
|
3880
|
+
[pathKey]: existing ? `${dir}${import_node_path6.default.win32.delimiter}${existing}` : dir
|
|
3745
3881
|
};
|
|
3746
3882
|
}
|
|
3747
3883
|
async function spawnCommand(cmd, args = [], options = {}) {
|
|
@@ -3842,22 +3978,30 @@ function llevenshteinDistance(s, t) {
|
|
|
3842
3978
|
}
|
|
3843
3979
|
return arr[t.length][s.length];
|
|
3844
3980
|
}
|
|
3845
|
-
var import_node_fs7, import_node_os2,
|
|
3981
|
+
var import_node_fs7, import_node_os2, import_node_path6, import_node_crypto3, import_promises, import_node_dns, import_node_net, import_node_child_process4, axiosModule, FETCH_BINARY_DEFAULTS, FETCH_TEXT_DEFAULTS, BACKGROUND_BUFFER_LIMIT, PTY_PACKAGE, READY_POLL_INTERVAL_MS, TRANSIENT_SESSION_ERROR, SESSION_TIMEOUT_ABORT, BROWSER_ERROR_PAGE, INITIAL_BLANK_DOCUMENT, TRANSIENT_PROCESS_INIT_EXIT_CODES, SETTLE_CEILING_MAX_MS, PRESERVED_TEMP_ENTRIES, UrlGuardError, REDIRECT_STATUSES, VIEWPORT_TOLERANCE_PX, VIEWPORT_SETTLE_MS, DEFAULT_VIEWPORT, ENV_VAR_REGEX, VERIFIED_FORMAT_BY_EXT, VERIFIED_IMG_BODY, posixBashProbe;
|
|
3846
3982
|
var init_utils = __esm({
|
|
3847
3983
|
"dist/core/utils.js"() {
|
|
3848
3984
|
"use strict";
|
|
3849
3985
|
import_node_fs7 = __toESM(require("node:fs"), 1);
|
|
3850
3986
|
import_node_os2 = __toESM(require("node:os"), 1);
|
|
3851
|
-
|
|
3987
|
+
import_node_path6 = __toESM(require("node:path"), 1);
|
|
3852
3988
|
import_node_crypto3 = __toESM(require("node:crypto"), 1);
|
|
3853
3989
|
import_promises = __toESM(require("node:dns/promises"), 1);
|
|
3990
|
+
import_node_dns = __toESM(require("node:dns"), 1);
|
|
3854
3991
|
import_node_net = __toESM(require("node:net"), 1);
|
|
3855
3992
|
import_node_child_process4 = require("node:child_process");
|
|
3856
3993
|
init_loader();
|
|
3857
3994
|
init_windowsBash();
|
|
3995
|
+
init_heavyDeps();
|
|
3858
3996
|
init_ptyWatchdog();
|
|
3859
|
-
import_node_module3 = require("node:module");
|
|
3860
3997
|
init_verification();
|
|
3998
|
+
FETCH_BINARY_DEFAULTS = {
|
|
3999
|
+
responseType: "arraybuffer",
|
|
4000
|
+
timeout: 3e4,
|
|
4001
|
+
maxContentLength: 50 * 1024 * 1024,
|
|
4002
|
+
maxBodyLength: 50 * 1024 * 1024
|
|
4003
|
+
};
|
|
4004
|
+
FETCH_TEXT_DEFAULTS = { ...FETCH_BINARY_DEFAULTS, responseType: "text" };
|
|
3861
4005
|
BACKGROUND_BUFFER_LIMIT = 256 * 1024;
|
|
3862
4006
|
PTY_PACKAGE = "@homebridge/node-pty-prebuilt-multiarch";
|
|
3863
4007
|
READY_POLL_INTERVAL_MS = 500;
|
|
@@ -3884,13 +4028,9 @@ var init_utils = __esm({
|
|
|
3884
4028
|
"jre",
|
|
3885
4029
|
"ios"
|
|
3886
4030
|
]);
|
|
3887
|
-
|
|
3888
|
-
responseType: "arraybuffer",
|
|
3889
|
-
timeout: 3e4,
|
|
3890
|
-
maxContentLength: 50 * 1024 * 1024,
|
|
3891
|
-
maxBodyLength: 50 * 1024 * 1024,
|
|
3892
|
-
maxRedirects: 5
|
|
4031
|
+
UrlGuardError = class extends Error {
|
|
3893
4032
|
};
|
|
4033
|
+
REDIRECT_STATUSES = [301, 302, 303, 307, 308];
|
|
3894
4034
|
VIEWPORT_TOLERANCE_PX = 16;
|
|
3895
4035
|
VIEWPORT_SETTLE_MS = 2e3;
|
|
3896
4036
|
DEFAULT_VIEWPORT = { width: 1200, height: 800 };
|
|
@@ -3932,7 +4072,7 @@ __export(browsers_exports, {
|
|
|
3932
4072
|
verifyDriverBinary: () => verifyDriverBinary
|
|
3933
4073
|
});
|
|
3934
4074
|
function isAllowedDriverPath(binaryPath) {
|
|
3935
|
-
return typeof binaryPath === "string" &&
|
|
4075
|
+
return typeof binaryPath === "string" && import_node_path7.default.isAbsolute(binaryPath) && ALLOWED_DRIVER_PATH.test(binaryPath);
|
|
3936
4076
|
}
|
|
3937
4077
|
function parseDriverVersion(output) {
|
|
3938
4078
|
const m = output.match(/(\d+\.\d+(?:\.\d+)*)/);
|
|
@@ -4104,12 +4244,12 @@ async function ensureBrowserInstalledImpl(name, options = {}) {
|
|
|
4104
4244
|
}
|
|
4105
4245
|
if (!force && existing && isStillFresh(existing.latestCheckedAt, now)) {
|
|
4106
4246
|
if (existing.latestKnownVersion === existing.installedVersion) {
|
|
4107
|
-
const
|
|
4108
|
-
return { path:
|
|
4247
|
+
const path34 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
|
|
4248
|
+
return { path: path34, version: existing.installedVersion, outdated: false };
|
|
4109
4249
|
}
|
|
4110
|
-
const
|
|
4250
|
+
const path33 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
|
|
4111
4251
|
logger(`${name} ${existing.installedVersion} installed in ${cacheDir}; "${BROWSER_CHANNELS[name]}" channel is now ${existing.latestKnownVersion}. Run \`doc-detective install browsers ${name} --force\` to update.`, "warn");
|
|
4112
|
-
return { path:
|
|
4252
|
+
return { path: path33, version: existing.installedVersion, outdated: true };
|
|
4113
4253
|
}
|
|
4114
4254
|
let latest;
|
|
4115
4255
|
try {
|
|
@@ -4117,8 +4257,8 @@ async function ensureBrowserInstalledImpl(name, options = {}) {
|
|
|
4117
4257
|
} catch (err) {
|
|
4118
4258
|
logger(`Channel resolution for ${name} skipped: ${String(err)}`, "debug");
|
|
4119
4259
|
if (existing) {
|
|
4120
|
-
const
|
|
4121
|
-
return { path:
|
|
4260
|
+
const path33 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
|
|
4261
|
+
return { path: path33, version: existing.installedVersion, outdated: false };
|
|
4122
4262
|
}
|
|
4123
4263
|
throw err;
|
|
4124
4264
|
}
|
|
@@ -4129,8 +4269,8 @@ async function ensureBrowserInstalledImpl(name, options = {}) {
|
|
|
4129
4269
|
latestCheckedAt: now.toISOString()
|
|
4130
4270
|
};
|
|
4131
4271
|
writeInstalledRecord(record, ctx);
|
|
4132
|
-
const
|
|
4133
|
-
return { path:
|
|
4272
|
+
const path33 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
|
|
4273
|
+
return { path: path33, version: latest, outdated: false };
|
|
4134
4274
|
}
|
|
4135
4275
|
if (!force && existing) {
|
|
4136
4276
|
record.browsers[name] = {
|
|
@@ -4140,8 +4280,8 @@ async function ensureBrowserInstalledImpl(name, options = {}) {
|
|
|
4140
4280
|
};
|
|
4141
4281
|
writeInstalledRecord(record, ctx);
|
|
4142
4282
|
logger(`${name} ${existing.installedVersion} installed in ${cacheDir}; "${BROWSER_CHANNELS[name]}" channel is now ${latest}. Run \`doc-detective install browsers ${name} --force\` to update.`, "warn");
|
|
4143
|
-
const
|
|
4144
|
-
return { path:
|
|
4283
|
+
const path33 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
|
|
4284
|
+
return { path: path33, version: existing.installedVersion, outdated: true };
|
|
4145
4285
|
}
|
|
4146
4286
|
logger(`Installing ${name} ${latest} into ${cacheDir}`, "info");
|
|
4147
4287
|
await browsersModule.install({
|
|
@@ -4149,9 +4289,9 @@ async function ensureBrowserInstalledImpl(name, options = {}) {
|
|
|
4149
4289
|
buildId: latest,
|
|
4150
4290
|
cacheDir
|
|
4151
4291
|
});
|
|
4152
|
-
let
|
|
4153
|
-
if (name === "chromedriver" && isAllowedDriverPath(
|
|
4154
|
-
let verify = await verifyDriverBinary(name,
|
|
4292
|
+
let path32 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
|
|
4293
|
+
if (name === "chromedriver" && isAllowedDriverPath(path32)) {
|
|
4294
|
+
let verify = await verifyDriverBinary(name, path32, { exec: deps.verifyExec });
|
|
4155
4295
|
if (!verify.ok) {
|
|
4156
4296
|
logger(`Installed ${name} ${latest} failed validation (${verify.error}); reinstalling once.`, "warn");
|
|
4157
4297
|
try {
|
|
@@ -4160,11 +4300,11 @@ async function ensureBrowserInstalledImpl(name, options = {}) {
|
|
|
4160
4300
|
logger(`Failed to prune broken ${name} ${latest}: ${String(err)}`, "debug");
|
|
4161
4301
|
}
|
|
4162
4302
|
await browsersModule.install({ browser: name, buildId: latest, cacheDir });
|
|
4163
|
-
|
|
4164
|
-
verify = await verifyDriverBinary(name,
|
|
4303
|
+
path32 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
|
|
4304
|
+
verify = await verifyDriverBinary(name, path32, { exec: deps.verifyExec });
|
|
4165
4305
|
}
|
|
4166
4306
|
if (!verify.ok) {
|
|
4167
|
-
throw new Error(`${name} ${latest} is present but non-functional after a reinstall (${verify.error}). It may be a partial or corrupt download; delete ${
|
|
4307
|
+
throw new Error(`${name} ${latest} is present but non-functional after a reinstall (${verify.error}). It may be a partial or corrupt download; delete ${path32} or reinstall.`);
|
|
4168
4308
|
}
|
|
4169
4309
|
}
|
|
4170
4310
|
if (existing && existing.installedVersion !== latest) {
|
|
@@ -4185,7 +4325,7 @@ async function ensureBrowserInstalledImpl(name, options = {}) {
|
|
|
4185
4325
|
latestCheckedAt: now.toISOString()
|
|
4186
4326
|
};
|
|
4187
4327
|
writeInstalledRecord(record, ctx);
|
|
4188
|
-
return { path:
|
|
4328
|
+
return { path: path32, version: latest, outdated: false };
|
|
4189
4329
|
}
|
|
4190
4330
|
async function locateExecutable(browsersModule, name, buildId, cacheDir, platform) {
|
|
4191
4331
|
if (typeof browsersModule.computeExecutablePath === "function") {
|
|
@@ -4218,9 +4358,9 @@ async function withGeckodriverCacheDir(cacheDir, fn) {
|
|
|
4218
4358
|
}
|
|
4219
4359
|
}
|
|
4220
4360
|
function isPathInside(child, dir) {
|
|
4221
|
-
const resolvedDir =
|
|
4222
|
-
const resolvedChild =
|
|
4223
|
-
return resolvedChild === resolvedDir || resolvedChild.startsWith(resolvedDir +
|
|
4361
|
+
const resolvedDir = import_node_path7.default.resolve(dir);
|
|
4362
|
+
const resolvedChild = import_node_path7.default.resolve(child);
|
|
4363
|
+
return resolvedChild === resolvedDir || resolvedChild.startsWith(resolvedDir + import_node_path7.default.sep);
|
|
4224
4364
|
}
|
|
4225
4365
|
function compareGeckodriverVersionsDesc(a, b) {
|
|
4226
4366
|
const pa = a.split(".");
|
|
@@ -4239,7 +4379,7 @@ function isRegularFileEntry(dir, entry) {
|
|
|
4239
4379
|
if (!entry.isSymbolicLink())
|
|
4240
4380
|
return false;
|
|
4241
4381
|
try {
|
|
4242
|
-
return import_node_fs8.default.statSync(
|
|
4382
|
+
return import_node_fs8.default.statSync(import_node_path7.default.join(dir, entry.name)).isFile();
|
|
4243
4383
|
} catch {
|
|
4244
4384
|
return false;
|
|
4245
4385
|
}
|
|
@@ -4253,7 +4393,7 @@ function readDirEntries(dir) {
|
|
|
4253
4393
|
}
|
|
4254
4394
|
function bareBinaryIn(dir, binName, entries) {
|
|
4255
4395
|
const match = entries.find((entry) => entry.name === binName && isRegularFileEntry(dir, entry));
|
|
4256
|
-
return match ?
|
|
4396
|
+
return match ? import_node_path7.default.join(dir, binName) : void 0;
|
|
4257
4397
|
}
|
|
4258
4398
|
function newestVersionedIn(dir, entries) {
|
|
4259
4399
|
const wantExe = process.platform === "win32";
|
|
@@ -4267,7 +4407,7 @@ function newestVersionedIn(dir, entries) {
|
|
|
4267
4407
|
if (wantExe !== Boolean(match[2]))
|
|
4268
4408
|
continue;
|
|
4269
4409
|
if (!best || compareGeckodriverVersionsDesc(match[1], best.version) < 0) {
|
|
4270
|
-
best = { version: match[1], file:
|
|
4410
|
+
best = { version: match[1], file: import_node_path7.default.join(dir, entry.name) };
|
|
4271
4411
|
}
|
|
4272
4412
|
}
|
|
4273
4413
|
return best;
|
|
@@ -4280,7 +4420,7 @@ function geckodriverBinaryInCache(cacheDir) {
|
|
|
4280
4420
|
for (const entry of rootEntries) {
|
|
4281
4421
|
if (!entry.isDirectory())
|
|
4282
4422
|
continue;
|
|
4283
|
-
const dir =
|
|
4423
|
+
const dir = import_node_path7.default.join(cacheDir, entry.name);
|
|
4284
4424
|
const nestedEntries = readDirEntries(dir);
|
|
4285
4425
|
const nestedVersioned = newestVersionedIn(dir, nestedEntries);
|
|
4286
4426
|
if (nestedVersioned && (!bestVersioned || compareGeckodriverVersionsDesc(nestedVersioned.version, bestVersioned.version) < 0)) {
|
|
@@ -4360,14 +4500,14 @@ function getInstalledBrowsers(ctx = {}) {
|
|
|
4360
4500
|
latestKnownVersion: entry.latestKnownVersion
|
|
4361
4501
|
}));
|
|
4362
4502
|
}
|
|
4363
|
-
var import_node_child_process5, import_node_fs8, import_node_os3,
|
|
4503
|
+
var import_node_child_process5, import_node_fs8, import_node_os3, import_node_path7, DRIVER_VERSION_ARGS, DRIVER_VERIFY_TIMEOUT_MS, ALLOWED_DRIVER_PATH, EXECUTABLE_HEADER_BYTES, ELF_MACHINE_BY_NODE_ARCH, ELF_MACHINE_LABELS, defaultDriverExec, BROWSER_CHANNELS, FRESHNESS_TTL_MS, RESOLVE_TIMEOUT_MS, RUNTIME_DEBUG2, defaultLogger2, inFlightInstalls2, geckodriverEnvQueue, GECKODRIVER_VERSIONED_NAME;
|
|
4364
4504
|
var init_browsers = __esm({
|
|
4365
4505
|
"dist/runtime/browsers.js"() {
|
|
4366
4506
|
"use strict";
|
|
4367
4507
|
import_node_child_process5 = require("node:child_process");
|
|
4368
4508
|
import_node_fs8 = __toESM(require("node:fs"), 1);
|
|
4369
4509
|
import_node_os3 = __toESM(require("node:os"), 1);
|
|
4370
|
-
|
|
4510
|
+
import_node_path7 = __toESM(require("node:path"), 1);
|
|
4371
4511
|
init_cacheDir();
|
|
4372
4512
|
init_loader();
|
|
4373
4513
|
DRIVER_VERSION_ARGS = {
|
|
@@ -4429,7 +4569,7 @@ var init_browsers = __esm({
|
|
|
4429
4569
|
function useDriversArgs(appiumHome, packages) {
|
|
4430
4570
|
let manifestText;
|
|
4431
4571
|
try {
|
|
4432
|
-
manifestText = import_node_fs9.default.readFileSync(
|
|
4572
|
+
manifestText = import_node_fs9.default.readFileSync(import_node_path8.default.join(appiumHome, "node_modules", ".cache", "appium", "extensions.yaml"), "utf8");
|
|
4433
4573
|
} catch {
|
|
4434
4574
|
}
|
|
4435
4575
|
let registered;
|
|
@@ -4442,7 +4582,7 @@ function useDriversArgs(appiumHome, packages) {
|
|
|
4442
4582
|
}
|
|
4443
4583
|
const names = packages.flatMap((pkg) => {
|
|
4444
4584
|
try {
|
|
4445
|
-
const name = JSON.parse(import_node_fs9.default.readFileSync(
|
|
4585
|
+
const name = JSON.parse(import_node_fs9.default.readFileSync(import_node_path8.default.join(appiumHome, "node_modules", pkg, "package.json"), "utf8"))?.appium?.driverName;
|
|
4446
4586
|
return typeof name === "string" && name ? [{ name, pkg }] : [];
|
|
4447
4587
|
} catch {
|
|
4448
4588
|
return [];
|
|
@@ -4461,26 +4601,26 @@ function browserPoolDriverPackages(platform) {
|
|
|
4461
4601
|
];
|
|
4462
4602
|
}
|
|
4463
4603
|
function appiumHomeForDriverPath(driverEntry) {
|
|
4464
|
-
const marker = `${
|
|
4604
|
+
const marker = `${import_node_path8.default.sep}node_modules${import_node_path8.default.sep}`;
|
|
4465
4605
|
const idx = driverEntry.lastIndexOf(marker);
|
|
4466
4606
|
return idx === -1 ? null : driverEntry.slice(0, idx);
|
|
4467
4607
|
}
|
|
4468
4608
|
function setAppiumHome(ctx = {}) {
|
|
4469
4609
|
process.env.APPIUM_HOME = getRuntimeDir({ cacheDir: ctx.cacheDir });
|
|
4470
4610
|
}
|
|
4471
|
-
var import_node_fs9,
|
|
4611
|
+
var import_node_fs9, import_node_path8;
|
|
4472
4612
|
var init_appium = __esm({
|
|
4473
4613
|
"dist/core/appium.js"() {
|
|
4474
4614
|
"use strict";
|
|
4475
4615
|
import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
4476
|
-
|
|
4616
|
+
import_node_path8 = __toESM(require("node:path"), 1);
|
|
4477
4617
|
init_cacheDir();
|
|
4478
4618
|
init_lazyYaml();
|
|
4479
4619
|
}
|
|
4480
4620
|
});
|
|
4481
4621
|
|
|
4482
4622
|
// dist/core/files.js
|
|
4483
|
-
async function readFile({ fileURLOrPath }) {
|
|
4623
|
+
async function readFile({ fileURLOrPath, throwOnError = false }) {
|
|
4484
4624
|
if (!fileURLOrPath) {
|
|
4485
4625
|
throw new Error("fileURLOrPath is required");
|
|
4486
4626
|
}
|
|
@@ -4497,40 +4637,55 @@ async function readFile({ fileURLOrPath }) {
|
|
|
4497
4637
|
isRemote = parsedURL.protocol === "http:" || parsedURL.protocol === "https:";
|
|
4498
4638
|
} catch (error) {
|
|
4499
4639
|
}
|
|
4640
|
+
const fail = (message) => {
|
|
4641
|
+
if (throwOnError)
|
|
4642
|
+
throw new Error(message);
|
|
4643
|
+
log({}, "warning", message);
|
|
4644
|
+
return null;
|
|
4645
|
+
};
|
|
4500
4646
|
if (isRemote) {
|
|
4501
4647
|
try {
|
|
4502
4648
|
const response = await (await import("axios")).default.get(fileURLOrPath);
|
|
4503
4649
|
content = response.data;
|
|
4504
4650
|
} catch (error) {
|
|
4505
|
-
|
|
4506
|
-
return null;
|
|
4651
|
+
return fail(`Couldn't read ${redactUrlForOutput(fileURLOrPath)}${reason(error.message)}`);
|
|
4507
4652
|
}
|
|
4508
4653
|
} else {
|
|
4509
4654
|
try {
|
|
4510
4655
|
content = await import_node_fs10.default.promises.readFile(fileURLOrPath, "utf8");
|
|
4511
4656
|
} catch (error) {
|
|
4512
|
-
|
|
4513
|
-
console.warn(`File not found: ${fileURLOrPath}`);
|
|
4514
|
-
} else {
|
|
4515
|
-
console.warn(`Error reading file: ${error.message}`);
|
|
4516
|
-
}
|
|
4517
|
-
return null;
|
|
4657
|
+
return fail(`Couldn't read ${import_node_path9.default.resolve(fileURLOrPath)}${error.code === "ENOENT" ? ": the file doesn't exist." : reason(error.message)}`);
|
|
4518
4658
|
}
|
|
4519
4659
|
}
|
|
4520
|
-
|
|
4660
|
+
return parseFileContent(content, fileURLOrPath, { throwOnError });
|
|
4661
|
+
}
|
|
4662
|
+
function locationOf(fileURLOrPath) {
|
|
4663
|
+
return /^https?:\/\//i.test(fileURLOrPath) ? redactUrlForOutput(fileURLOrPath) : import_node_path9.default.resolve(fileURLOrPath);
|
|
4664
|
+
}
|
|
4665
|
+
function parseFileContent(content, fileURLOrPath, { throwOnError = false } = {}) {
|
|
4666
|
+
if (typeof content !== "string")
|
|
4667
|
+
return content;
|
|
4668
|
+
const name = /^https?:\/\//i.test(fileURLOrPath) ? new URL(fileURLOrPath).pathname : fileURLOrPath;
|
|
4669
|
+
const ext = name.split(".").pop()?.toLowerCase();
|
|
4670
|
+
const failed = (format, error) => {
|
|
4671
|
+
if (throwOnError) {
|
|
4672
|
+
const cause = String(error.message).split("\n")[0].replace(/:$/, "");
|
|
4673
|
+
throw new Error(`Couldn't read ${locationOf(fileURLOrPath)}: it isn't valid ${format} (${cause}).`);
|
|
4674
|
+
}
|
|
4675
|
+
log({}, "warning", `Failed to parse ${format}: ${error.message}`);
|
|
4676
|
+
return content;
|
|
4677
|
+
};
|
|
4521
4678
|
if (ext === "json") {
|
|
4522
4679
|
try {
|
|
4523
4680
|
return JSON.parse(content);
|
|
4524
4681
|
} catch (error) {
|
|
4525
|
-
|
|
4526
|
-
return content;
|
|
4682
|
+
return failed("JSON", error);
|
|
4527
4683
|
}
|
|
4528
4684
|
} else if (ext === "yaml" || ext === "yml") {
|
|
4529
4685
|
try {
|
|
4530
4686
|
return requireYaml().parse(content);
|
|
4531
4687
|
} catch (error) {
|
|
4532
|
-
|
|
4533
|
-
return content;
|
|
4688
|
+
return failed("YAML", error);
|
|
4534
4689
|
}
|
|
4535
4690
|
} else {
|
|
4536
4691
|
return content;
|
|
@@ -4579,13 +4734,13 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
|
|
|
4579
4734
|
if (relativePath.startsWith("https://") || relativePath.startsWith("http://") || relativePath.startsWith("heretto:")) {
|
|
4580
4735
|
return relativePath;
|
|
4581
4736
|
}
|
|
4582
|
-
if (
|
|
4737
|
+
if (import_node_path9.default.isAbsolute(relativePath)) {
|
|
4583
4738
|
return relativePath;
|
|
4584
4739
|
}
|
|
4585
4740
|
const fileExists = import_node_fs10.default.existsSync(filePath2);
|
|
4586
|
-
const isFile = fileExists ? import_node_fs10.default.lstatSync(filePath2).isFile() :
|
|
4587
|
-
const basePath = isFile ?
|
|
4588
|
-
return baseType === "file" ?
|
|
4741
|
+
const isFile = fileExists ? import_node_fs10.default.lstatSync(filePath2).isFile() : import_node_path9.default.parse(filePath2).ext !== "";
|
|
4742
|
+
const basePath = isFile ? import_node_path9.default.dirname(filePath2) : filePath2;
|
|
4743
|
+
return baseType === "file" ? import_node_path9.default.resolve(basePath, relativePath) : import_node_path9.default.resolve(relativePath);
|
|
4589
4744
|
}
|
|
4590
4745
|
const relativePathBase = config.relativePathBase;
|
|
4591
4746
|
let pathProperties;
|
|
@@ -4635,7 +4790,7 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
|
|
|
4635
4790
|
isStep: property === "steps"
|
|
4636
4791
|
});
|
|
4637
4792
|
} else if (typeof item === "string" && pathProperties.includes(property)) {
|
|
4638
|
-
const resolved = property === "path" && object.directory &&
|
|
4793
|
+
const resolved = property === "path" && object.directory && import_node_path9.default.isAbsolute(object.directory) ? resolve(relativePathBase, item, object.directory) : resolve(relativePathBase, item, filePath);
|
|
4639
4794
|
object[property][i] = resolved;
|
|
4640
4795
|
}
|
|
4641
4796
|
}
|
|
@@ -4653,7 +4808,7 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
|
|
|
4653
4808
|
}
|
|
4654
4809
|
if (pathProperties.includes(property) || isStep && stepShorthandPathProperties.includes(property)) {
|
|
4655
4810
|
if (property === "path" && object.directory) {
|
|
4656
|
-
const directory =
|
|
4811
|
+
const directory = import_node_path9.default.isAbsolute(object.directory) ? object.directory : resolve(relativePathBase, object.directory, filePath);
|
|
4657
4812
|
object[property] = resolve(relativePathBase, object[property], directory);
|
|
4658
4813
|
} else {
|
|
4659
4814
|
object[property] = resolve(relativePathBase, object[property], filePath);
|
|
@@ -4663,14 +4818,19 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
|
|
|
4663
4818
|
}
|
|
4664
4819
|
return object;
|
|
4665
4820
|
}
|
|
4666
|
-
var import_node_fs10,
|
|
4821
|
+
var import_node_fs10, import_node_path9, reason;
|
|
4667
4822
|
var init_files = __esm({
|
|
4668
4823
|
"dist/core/files.js"() {
|
|
4669
4824
|
"use strict";
|
|
4670
4825
|
import_node_fs10 = __toESM(require("node:fs"), 1);
|
|
4671
|
-
|
|
4826
|
+
import_node_path9 = __toESM(require("node:path"), 1);
|
|
4672
4827
|
init_validate();
|
|
4673
4828
|
init_lazyYaml();
|
|
4829
|
+
init_utils();
|
|
4830
|
+
reason = (text) => {
|
|
4831
|
+
const trimmed2 = text?.trim();
|
|
4832
|
+
return !trimmed2 ? "." : `: ${/[.!?]$/.test(trimmed2) ? trimmed2 : `${trimmed2}.`}`;
|
|
4833
|
+
};
|
|
4674
4834
|
}
|
|
4675
4835
|
});
|
|
4676
4836
|
|
|
@@ -4685,9 +4845,75 @@ async function loadDescription(descriptionPath = "") {
|
|
|
4685
4845
|
if (!descriptionPath) {
|
|
4686
4846
|
throw new Error("Description is required.");
|
|
4687
4847
|
}
|
|
4688
|
-
const
|
|
4689
|
-
|
|
4690
|
-
const
|
|
4848
|
+
const guard = localUrlPolicy() !== "allow";
|
|
4849
|
+
let lastPublic;
|
|
4850
|
+
const docName = (given, final) => {
|
|
4851
|
+
const [a, b] = [given, final].map(redactUrlForOutput);
|
|
4852
|
+
return a === b ? a : `${a} (redirected to ${b})`;
|
|
4853
|
+
};
|
|
4854
|
+
let definition;
|
|
4855
|
+
if (guard && /^https?:\/\//i.test(descriptionPath)) {
|
|
4856
|
+
const root = await getGuarded(descriptionPath, FETCH_TEXT_DEFAULTS);
|
|
4857
|
+
const text = root.data;
|
|
4858
|
+
let content = text;
|
|
4859
|
+
try {
|
|
4860
|
+
content = JSON.parse(text);
|
|
4861
|
+
} catch {
|
|
4862
|
+
}
|
|
4863
|
+
definition = parseFileContent(content, descriptionPath, { throwOnError: true });
|
|
4864
|
+
if (root.reachedPublic)
|
|
4865
|
+
lastPublic = docName(descriptionPath, root.url);
|
|
4866
|
+
} else {
|
|
4867
|
+
definition = await readFile({ fileURLOrPath: descriptionPath, throwOnError: true });
|
|
4868
|
+
}
|
|
4869
|
+
if (typeof definition !== "object" || definition === null || Array.isArray(definition)) {
|
|
4870
|
+
throw new Error(`Couldn't read ${locationOf(descriptionPath)}: it's empty or isn't a JSON or YAML object.`);
|
|
4871
|
+
}
|
|
4872
|
+
const refused = (ref, doc, target) => new UrlGuardError(`Couldn't resolve $ref ${ref} in ${doc}: a public document can't refer to ${target}.
|
|
4873
|
+
Hint: If you trust this document, set DOC_DETECTIVE_ALLOW_LOCAL_URLS=1, or download it and point descriptionPath at the local copy.`);
|
|
4874
|
+
const { default: parser, FileResolver } = await import("@apidevtools/json-schema-ref-parser");
|
|
4875
|
+
if (typeof FileResolver.read !== "function") {
|
|
4876
|
+
throw new Error("Couldn't load the OpenAPI description: json-schema-ref-parser's FileResolver.read isn't a function.");
|
|
4877
|
+
}
|
|
4878
|
+
const readLocalFile = FileResolver.read.bind(FileResolver);
|
|
4879
|
+
const dereferencedDefinition = await parser.dereference(definition, {
|
|
4880
|
+
resolve: {
|
|
4881
|
+
file: {
|
|
4882
|
+
async read(file) {
|
|
4883
|
+
if (lastPublic) {
|
|
4884
|
+
throw refused(redactUrlForOutput(file.url.split("#")[0]), lastPublic, "a local file");
|
|
4885
|
+
}
|
|
4886
|
+
return readLocalFile(file);
|
|
4887
|
+
}
|
|
4888
|
+
},
|
|
4889
|
+
http: {
|
|
4890
|
+
// The parser's own guard refuses every private address, even from a
|
|
4891
|
+
// local description. getGuarded applies Doc Detective's policy instead.
|
|
4892
|
+
safeUrlResolver: false,
|
|
4893
|
+
async read(file) {
|
|
4894
|
+
const ref = redactUrlForOutput(file.url);
|
|
4895
|
+
const doc = lastPublic;
|
|
4896
|
+
if (doc) {
|
|
4897
|
+
let address;
|
|
4898
|
+
try {
|
|
4899
|
+
address = await privateAddressOf(file.url);
|
|
4900
|
+
} catch (error) {
|
|
4901
|
+
if (!error.host)
|
|
4902
|
+
throw error;
|
|
4903
|
+
throw new Error(`Couldn't resolve $ref ${ref} in ${doc}: couldn't resolve host ${error.host} (${error.code}).`);
|
|
4904
|
+
}
|
|
4905
|
+
if (address)
|
|
4906
|
+
throw refused(ref, doc, `${address}, a private address`);
|
|
4907
|
+
}
|
|
4908
|
+
const refuseRef = doc ? (privateAddress) => refused(ref, doc, `${privateAddress}, a private address`) : void 0;
|
|
4909
|
+
const { data, url, reachedPublic } = await getGuarded(file.url, FETCH_TEXT_DEFAULTS, refuseRef, refuseRef && ((_from, privateAddress) => refuseRef(privateAddress)));
|
|
4910
|
+
if (guard && reachedPublic)
|
|
4911
|
+
lastPublic = docName(file.url, url);
|
|
4912
|
+
return data;
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
});
|
|
4691
4917
|
return dereferencedDefinition;
|
|
4692
4918
|
}
|
|
4693
4919
|
async function getOperation(definition = {}, operationId = "", responseCode = "", exampleKey = "", server = "") {
|
|
@@ -4697,10 +4923,10 @@ async function getOperation(definition = {}, operationId = "", responseCode = ""
|
|
|
4697
4923
|
if (!operationId) {
|
|
4698
4924
|
throw new Error("OperationId is required.");
|
|
4699
4925
|
}
|
|
4700
|
-
for (const
|
|
4701
|
-
for (const method in definition.paths[
|
|
4702
|
-
if (definition.paths[
|
|
4703
|
-
const operation = definition.paths[
|
|
4926
|
+
for (const path32 in definition.paths) {
|
|
4927
|
+
for (const method in definition.paths[path32]) {
|
|
4928
|
+
if (definition.paths[path32][method].operationId === operationId) {
|
|
4929
|
+
const operation = definition.paths[path32][method];
|
|
4704
4930
|
if (!server) {
|
|
4705
4931
|
if (definition.servers && definition.servers.length > 0) {
|
|
4706
4932
|
server = definition.servers[0].url;
|
|
@@ -4708,9 +4934,9 @@ async function getOperation(definition = {}, operationId = "", responseCode = ""
|
|
|
4708
4934
|
throw new Error("No server URL provided and no servers defined in the OpenAPI definition.");
|
|
4709
4935
|
}
|
|
4710
4936
|
}
|
|
4711
|
-
const example = await compileExample(operation, server +
|
|
4937
|
+
const example = await compileExample(operation, server + path32, responseCode, exampleKey);
|
|
4712
4938
|
const schemas = getSchemas(operation, responseCode);
|
|
4713
|
-
return { path:
|
|
4939
|
+
return { path: path32, method, definition: operation, schemas, example };
|
|
4714
4940
|
}
|
|
4715
4941
|
}
|
|
4716
4942
|
}
|
|
@@ -4731,15 +4957,15 @@ function getSchemas(definition = {}, responseCode = "") {
|
|
|
4731
4957
|
schemas.response = definition.responses[responseCode].content[Object.keys(definition.responses[responseCode].content)[0]].schema;
|
|
4732
4958
|
return schemas;
|
|
4733
4959
|
}
|
|
4734
|
-
async function compileExample(operation = {},
|
|
4960
|
+
async function compileExample(operation = {}, path32 = "", responseCode = "", exampleKey = "") {
|
|
4735
4961
|
if (!operation) {
|
|
4736
4962
|
throw new Error("Operation is required.");
|
|
4737
4963
|
}
|
|
4738
|
-
if (!
|
|
4964
|
+
if (!path32) {
|
|
4739
4965
|
throw new Error("Path is required.");
|
|
4740
4966
|
}
|
|
4741
4967
|
let example = {
|
|
4742
|
-
url:
|
|
4968
|
+
url: path32,
|
|
4743
4969
|
request: { parameters: {}, headers: {}, body: {} },
|
|
4744
4970
|
response: { headers: {}, body: {} }
|
|
4745
4971
|
};
|
|
@@ -5104,6 +5330,9 @@ function getEnvironment() {
|
|
|
5104
5330
|
const environment = {};
|
|
5105
5331
|
environment.arch = import_node_os4.default.arch();
|
|
5106
5332
|
environment.platform = platformMap[process.platform];
|
|
5333
|
+
environment.docDetectiveVersion = ownVersion();
|
|
5334
|
+
environment.nodeVersion = process.versions.node;
|
|
5335
|
+
environment.osRelease = import_node_os4.default.release();
|
|
5107
5336
|
return environment;
|
|
5108
5337
|
}
|
|
5109
5338
|
function cacheKeyFor(config) {
|
|
@@ -5177,7 +5406,7 @@ function detectInstalledBrowserDrivers(config, isInstalled = (name) => Boolean(r
|
|
|
5177
5406
|
async function probeBrowserEnvironment({ config, browsersDir }) {
|
|
5178
5407
|
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
5179
5408
|
const cwd = process.cwd();
|
|
5180
|
-
process.chdir(
|
|
5409
|
+
process.chdir(packageRoot() ?? cwd);
|
|
5181
5410
|
let installedBrowsers = [];
|
|
5182
5411
|
let browserDetectionFailed = false;
|
|
5183
5412
|
let installedDrivers = { chromium: false, gecko: false, safari: false };
|
|
@@ -5393,7 +5622,7 @@ async function getBrowserDiagnostics({ config }) {
|
|
|
5393
5622
|
});
|
|
5394
5623
|
return { browsers, detectionFailed };
|
|
5395
5624
|
}
|
|
5396
|
-
var import_node_os4, import_node_fs11,
|
|
5625
|
+
var import_node_os4, import_node_fs11, import_node_child_process6, platformMap, defaultFileTypes, cachedAppsByDir;
|
|
5397
5626
|
var init_config = __esm({
|
|
5398
5627
|
"dist/core/config.js"() {
|
|
5399
5628
|
"use strict";
|
|
@@ -5401,15 +5630,13 @@ var init_config = __esm({
|
|
|
5401
5630
|
import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
5402
5631
|
init_validate();
|
|
5403
5632
|
init_utils();
|
|
5404
|
-
import_node_path9 = __toESM(require("node:path"), 1);
|
|
5405
5633
|
import_node_child_process6 = require("node:child_process");
|
|
5406
5634
|
init_loader();
|
|
5407
5635
|
init_cacheDir();
|
|
5636
|
+
init_heavyDeps();
|
|
5408
5637
|
init_browsers();
|
|
5409
5638
|
init_appium();
|
|
5410
5639
|
init_openapi();
|
|
5411
|
-
import_node_url4 = require("node:url");
|
|
5412
|
-
__dirname3 = import_node_path9.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
|
|
5413
5640
|
platformMap = {
|
|
5414
5641
|
darwin: "mac",
|
|
5415
5642
|
linux: "linux",
|
|
@@ -6684,10 +6911,9 @@ function sendTelemetry(config, command, results) {
|
|
|
6684
6911
|
if (config?.telemetry?.send === false)
|
|
6685
6912
|
return;
|
|
6686
6913
|
const telemetryData = process.env["DOC_DETECTIVE_META"] !== void 0 ? JSON.parse(process.env["DOC_DETECTIVE_META"]) : {};
|
|
6687
|
-
const pkg = require2("../../package.json");
|
|
6688
6914
|
telemetryData.distribution = telemetryData.distribution || "doc-detective";
|
|
6689
6915
|
telemetryData.dist_interface = telemetryData.dist_interface || "package";
|
|
6690
|
-
telemetryData.core_version =
|
|
6916
|
+
telemetryData.core_version = ownVersion() ?? "unknown";
|
|
6691
6917
|
telemetryData.dist_version = telemetryData.dist_version || telemetryData.core_version;
|
|
6692
6918
|
telemetryData.core_platform = platformMap2[import_node_os14.default.platform()] || import_node_os14.default.platform();
|
|
6693
6919
|
telemetryData.dist_platform = telemetryData.dist_platform || telemetryData.core_platform;
|
|
@@ -6706,14 +6932,14 @@ function sendTelemetry(config, command, results) {
|
|
|
6706
6932
|
Object.entries(value).forEach(([key, value2]) => {
|
|
6707
6933
|
if (typeof value2 === "object") {
|
|
6708
6934
|
Object.entries(value2).forEach(([key2, value22]) => {
|
|
6709
|
-
telemetryData[`${parentKey.
|
|
6935
|
+
telemetryData[`${parentKey.replaceAll(" ", "_")}_${key.replaceAll(" ", "_")}_${key2.replaceAll(" ", "_")}`] = value22;
|
|
6710
6936
|
});
|
|
6711
6937
|
} else {
|
|
6712
|
-
telemetryData[`${parentKey.
|
|
6938
|
+
telemetryData[`${parentKey.replaceAll(" ", "_")}_${key.replaceAll(" ", "_")}`] = value2;
|
|
6713
6939
|
}
|
|
6714
6940
|
});
|
|
6715
6941
|
} else {
|
|
6716
|
-
telemetryData[parentKey.
|
|
6942
|
+
telemetryData[parentKey.replaceAll(" ", "_")] = value;
|
|
6717
6943
|
}
|
|
6718
6944
|
});
|
|
6719
6945
|
}
|
|
@@ -6728,15 +6954,14 @@ async function awaitTelemetryFlush() {
|
|
|
6728
6954
|
if (pending)
|
|
6729
6955
|
await pending;
|
|
6730
6956
|
}
|
|
6731
|
-
var import_node_os14, import_posthog_node,
|
|
6957
|
+
var import_node_os14, import_posthog_node, platformMap2, TELEMETRY_FLUSH_TIMEOUT_MS, pendingTelemetryFlush;
|
|
6732
6958
|
var init_telem = __esm({
|
|
6733
6959
|
"dist/core/telem.js"() {
|
|
6734
6960
|
"use strict";
|
|
6735
6961
|
import_node_os14 = __toESM(require("node:os"), 1);
|
|
6736
6962
|
import_posthog_node = require("posthog-node");
|
|
6737
|
-
import_node_module5 = require("node:module");
|
|
6738
6963
|
init_telemLite();
|
|
6739
|
-
|
|
6964
|
+
init_heavyDeps();
|
|
6740
6965
|
platformMap2 = {
|
|
6741
6966
|
win32: "windows",
|
|
6742
6967
|
darwin: "mac",
|
|
@@ -6992,7 +7217,7 @@ var import_node_fs12 = __toESM(require("node:fs"), 1);
|
|
|
6992
7217
|
var import_node_path10 = __toESM(require("node:path"), 1);
|
|
6993
7218
|
var import_node_https2 = __toESM(require("node:https"), 1);
|
|
6994
7219
|
var import_node_http = __toESM(require("node:http"), 1);
|
|
6995
|
-
var
|
|
7220
|
+
var import_node_module3 = require("node:module");
|
|
6996
7221
|
var import_node_os5 = __toESM(require("node:os"), 1);
|
|
6997
7222
|
var import_node_crypto4 = __toESM(require("node:crypto"), 1);
|
|
6998
7223
|
var import_adm_zip = __toESM(require("adm-zip"), 1);
|
|
@@ -7773,7 +7998,7 @@ var HerettoUploader = class {
|
|
|
7773
7998
|
return contentTypes[ext] || "application/octet-stream";
|
|
7774
7999
|
}
|
|
7775
8000
|
};
|
|
7776
|
-
var requireFromHere2 = (0,
|
|
8001
|
+
var requireFromHere2 = (0, import_node_module3.createRequire)(importMetaUrl);
|
|
7777
8002
|
var requireAxios = () => requireFromHere2("axios");
|
|
7778
8003
|
var POLLING_INTERVAL_MS = 5e3;
|
|
7779
8004
|
var POLLING_TIMEOUT_MS = 3e5;
|
|
@@ -8745,7 +8970,8 @@ function resolveContexts({ contexts, test, config }) {
|
|
|
8745
8970
|
if (platform !== void 0)
|
|
8746
8971
|
staticContext.platform = platform;
|
|
8747
8972
|
if (browser !== void 0) {
|
|
8748
|
-
|
|
8973
|
+
const { version: _authored, ...authoredBrowser } = browser;
|
|
8974
|
+
staticContext.browser = browser.name === "safari" && !isMobileTargetPlatform(platform) ? { ...authoredBrowser, name: "webkit" } : authoredBrowser;
|
|
8749
8975
|
}
|
|
8750
8976
|
staticContexts.push(staticContext);
|
|
8751
8977
|
});
|
|
@@ -14816,11 +15042,11 @@ function hasUnresolvedMetaReference(expression, context) {
|
|
|
14816
15042
|
}
|
|
14817
15043
|
return false;
|
|
14818
15044
|
}
|
|
14819
|
-
function getMetaValue(
|
|
15045
|
+
function getMetaValue(path32, context) {
|
|
14820
15046
|
if (!context) {
|
|
14821
15047
|
return void 0;
|
|
14822
15048
|
}
|
|
14823
|
-
const [basePath, jsonPointer] =
|
|
15049
|
+
const [basePath, jsonPointer] = path32.split("#");
|
|
14824
15050
|
const resolvedPath = resolvePathTemplateVariables(basePath, context);
|
|
14825
15051
|
let value = getNestedProperty(context, resolvedPath);
|
|
14826
15052
|
if (jsonPointer && value) {
|
|
@@ -14837,9 +15063,9 @@ function getMetaValue(path31, context) {
|
|
|
14837
15063
|
}
|
|
14838
15064
|
return value;
|
|
14839
15065
|
}
|
|
14840
|
-
function resolvePathTemplateVariables(
|
|
15066
|
+
function resolvePathTemplateVariables(path32, context) {
|
|
14841
15067
|
const templateRegex = /\{\{(\w+)\}\}/g;
|
|
14842
|
-
return
|
|
15068
|
+
return path32.replace(templateRegex, (match, varName) => {
|
|
14843
15069
|
if (context && context.id && varName === "id") {
|
|
14844
15070
|
return context.id;
|
|
14845
15071
|
}
|
|
@@ -14876,10 +15102,10 @@ async function resolveEmbeddedExpressions(str, context) {
|
|
|
14876
15102
|
parts.push(str.slice(lastIdx));
|
|
14877
15103
|
return parts.join("");
|
|
14878
15104
|
}
|
|
14879
|
-
function getNestedProperty(obj,
|
|
14880
|
-
if (!obj || !
|
|
15105
|
+
function getNestedProperty(obj, path32) {
|
|
15106
|
+
if (!obj || !path32)
|
|
14881
15107
|
return void 0;
|
|
14882
|
-
const parts =
|
|
15108
|
+
const parts = path32.split(".");
|
|
14883
15109
|
let current = obj;
|
|
14884
15110
|
for (const part of parts) {
|
|
14885
15111
|
if (current === null || current === void 0)
|
|
@@ -16050,6 +16276,11 @@ async function wait({ config, step, driver }) {
|
|
|
16050
16276
|
return result;
|
|
16051
16277
|
}
|
|
16052
16278
|
|
|
16279
|
+
// dist/core/tests/timing.js
|
|
16280
|
+
var RECORDING_PACE_MS = 2e3;
|
|
16281
|
+
var RENDER_SETTLE_MS = 100;
|
|
16282
|
+
var RECORDER_SETTLE_MS = 500;
|
|
16283
|
+
|
|
16053
16284
|
// dist/core/tests/findElement.js
|
|
16054
16285
|
async function finalizeFound({ result }) {
|
|
16055
16286
|
const specs = [
|
|
@@ -16222,7 +16453,7 @@ async function findElement({ config, step, driver, click, appSession, processReg
|
|
|
16222
16453
|
}
|
|
16223
16454
|
}
|
|
16224
16455
|
if (isRecordingActive(driver)) {
|
|
16225
|
-
await wait({ config, step: { wait:
|
|
16456
|
+
await wait({ config, step: { wait: RECORDING_PACE_MS }, driver });
|
|
16226
16457
|
}
|
|
16227
16458
|
return result;
|
|
16228
16459
|
} else {
|
|
@@ -16325,7 +16556,7 @@ async function findElement({ config, step, driver, click, appSession, processReg
|
|
|
16325
16556
|
}
|
|
16326
16557
|
}
|
|
16327
16558
|
if (isRecordingActive(driver)) {
|
|
16328
|
-
await wait({ config, step: { wait:
|
|
16559
|
+
await wait({ config, step: { wait: RECORDING_PACE_MS }, driver });
|
|
16329
16560
|
}
|
|
16330
16561
|
return result;
|
|
16331
16562
|
}
|
|
@@ -16960,7 +17191,7 @@ async function goTo({ config, step, driver }) {
|
|
|
16960
17191
|
}, { timeout: waitTimeout });
|
|
16961
17192
|
waitResults.documentReady.passed = true;
|
|
16962
17193
|
waitResults.documentReady.message = "Document ready";
|
|
16963
|
-
await driver.pause(
|
|
17194
|
+
await driver.pause(RENDER_SETTLE_MS);
|
|
16964
17195
|
const elapsedTime = Date.now() - waitStartTime;
|
|
16965
17196
|
const remainingTimeout = waitTimeout - elapsedTime;
|
|
16966
17197
|
if (remainingTimeout <= 0) {
|
|
@@ -17185,10 +17416,13 @@ async function startBackgroundProcessSurface({ config, descriptor, processRegist
|
|
|
17185
17416
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
17186
17417
|
let bg;
|
|
17187
17418
|
if (descriptor.tty) {
|
|
17419
|
+
const size = typeof descriptor.tty === "object" ? descriptor.tty : {};
|
|
17188
17420
|
try {
|
|
17189
17421
|
bg = await d.spawnPtyBackgroundCommand(descriptor.command, args, {
|
|
17190
17422
|
...bgOptions,
|
|
17191
|
-
cacheDir: config?.cacheDir
|
|
17423
|
+
cacheDir: config?.cacheDir,
|
|
17424
|
+
cols: size.cols,
|
|
17425
|
+
rows: size.rows
|
|
17192
17426
|
});
|
|
17193
17427
|
} catch (error) {
|
|
17194
17428
|
if (error?.code !== "NODE_PTY_UNAVAILABLE") {
|
|
@@ -17478,13 +17712,13 @@ function shouldRetry(statusCode) {
|
|
|
17478
17712
|
return false;
|
|
17479
17713
|
return statusCode === 429 || statusCode >= 500;
|
|
17480
17714
|
}
|
|
17481
|
-
async function attemptRequest(method, url, headers) {
|
|
17715
|
+
async function attemptRequest(method, url, headers, timeout) {
|
|
17482
17716
|
try {
|
|
17483
17717
|
const res = await (await loadAxios()).request({
|
|
17484
17718
|
method,
|
|
17485
17719
|
url,
|
|
17486
17720
|
headers,
|
|
17487
|
-
timeout
|
|
17721
|
+
timeout,
|
|
17488
17722
|
maxRedirects: 5,
|
|
17489
17723
|
validateStatus: () => true
|
|
17490
17724
|
});
|
|
@@ -17497,7 +17731,7 @@ async function attemptRequest(method, url, headers) {
|
|
|
17497
17731
|
retryAfter: parseRetryAfter(error.response.headers?.["retry-after"] ?? error.response.headers?.["Retry-After"])
|
|
17498
17732
|
};
|
|
17499
17733
|
}
|
|
17500
|
-
return { statusCode: null, retryAfter: null };
|
|
17734
|
+
return { statusCode: null, retryAfter: null, errorCode: error?.code };
|
|
17501
17735
|
}
|
|
17502
17736
|
}
|
|
17503
17737
|
async function checkLink({ config, step }) {
|
|
@@ -17538,11 +17772,12 @@ async function checkLink({ config, step }) {
|
|
|
17538
17772
|
}
|
|
17539
17773
|
const headers = mergeHeaders(step.checkLink.headers);
|
|
17540
17774
|
const url = step.checkLink.url;
|
|
17775
|
+
const timeout = step.checkLink.timeout ?? 1e4;
|
|
17541
17776
|
const acceptedCodes = step.checkLink.statusCodes;
|
|
17542
17777
|
const isAccepted = (code) => code !== null && acceptedCodes.indexOf(code) >= 0;
|
|
17543
17778
|
let last = { statusCode: null, retryAfter: null };
|
|
17544
17779
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
17545
|
-
last = await attemptRequest("get", url, headers);
|
|
17780
|
+
last = await attemptRequest("get", url, headers, timeout);
|
|
17546
17781
|
if (isAccepted(last.statusCode))
|
|
17547
17782
|
break;
|
|
17548
17783
|
if (!shouldRetry(last.statusCode))
|
|
@@ -17553,14 +17788,14 @@ async function checkLink({ config, step }) {
|
|
|
17553
17788
|
}
|
|
17554
17789
|
}
|
|
17555
17790
|
if (!isAccepted(last.statusCode) && (last.statusCode === 429 || last.statusCode === 403)) {
|
|
17556
|
-
const headResult = await attemptRequest("head", url, headers);
|
|
17791
|
+
const headResult = await attemptRequest("head", url, headers, timeout);
|
|
17557
17792
|
if (isAccepted(headResult.statusCode)) {
|
|
17558
17793
|
last = headResult;
|
|
17559
17794
|
}
|
|
17560
17795
|
}
|
|
17561
17796
|
if (last.statusCode === null) {
|
|
17562
17797
|
result.status = "FAIL";
|
|
17563
|
-
result.description = `Invalid or unresolvable URL: ${url}`;
|
|
17798
|
+
result.description = last.errorCode === "ECONNABORTED" ? `Couldn't reach ${redactUrlForOutput(url)}: no response within ${timeout} ms (ECONNABORTED).` : `Invalid or unresolvable URL: ${redactUrlForOutput(url)}`;
|
|
17564
17799
|
return result;
|
|
17565
17800
|
}
|
|
17566
17801
|
result.outputs = { statusCode: last.statusCode };
|
|
@@ -18048,7 +18283,8 @@ async function captureScreenshot({ config, step, driver, appSession, processRegi
|
|
|
18048
18283
|
const fetched = await fetchFile(originalUrlPath, { binary: true });
|
|
18049
18284
|
if (fetched.result !== "success") {
|
|
18050
18285
|
result.status = "FAIL";
|
|
18051
|
-
|
|
18286
|
+
const msg = fetched.message?.message ?? String(fetched.message);
|
|
18287
|
+
result.description = fetched.message instanceof UrlGuardError ? msg : `Couldn't fetch remote reference image (${redactedUrl}): ${msg}`;
|
|
18052
18288
|
return result;
|
|
18053
18289
|
}
|
|
18054
18290
|
existFilePath = fetched.path;
|
|
@@ -18171,7 +18407,7 @@ async function captureScreenshot({ config, step, driver, appSession, processRegi
|
|
|
18171
18407
|
});
|
|
18172
18408
|
window.scrollBy(-pad.left, -pad.top);
|
|
18173
18409
|
}, element, padding);
|
|
18174
|
-
await driver.pause(
|
|
18410
|
+
await driver.pause(RENDER_SETTLE_MS);
|
|
18175
18411
|
}
|
|
18176
18412
|
const recordingActive = !isAppCapture && isRecordingActive(driver);
|
|
18177
18413
|
let captureBuffer;
|
|
@@ -19103,7 +19339,7 @@ async function startRecording({ config, context, step, driver, recordingHost, ap
|
|
|
19103
19339
|
proc.on("error", (err) => {
|
|
19104
19340
|
spawnError = err;
|
|
19105
19341
|
});
|
|
19106
|
-
await new Promise((r) => setTimeout(r,
|
|
19342
|
+
await new Promise((r) => setTimeout(r, RECORDER_SETTLE_MS));
|
|
19107
19343
|
if (spawnError || proc.exitCode !== null) {
|
|
19108
19344
|
result.status = "FAIL";
|
|
19109
19345
|
result.description = `Couldn't start ffmpeg recording.${spawnError ? ` ${spawnError}` : ` ffmpeg exited with code ${proc.exitCode}.`} On macOS, grant screen recording permission; on Linux, ensure a display (or Xvfb) is available.${stderrTail ? ` ffmpeg: ${stderrTail.trim().slice(-500)}` : ""}`;
|
|
@@ -19665,7 +19901,7 @@ async function waitForStableFile(filePath, maxSeconds) {
|
|
|
19665
19901
|
stableReads = 0;
|
|
19666
19902
|
}
|
|
19667
19903
|
lastSize = size;
|
|
19668
|
-
await new Promise((r) => setTimeout(r,
|
|
19904
|
+
await new Promise((r) => setTimeout(r, RECORDER_SETTLE_MS));
|
|
19669
19905
|
}
|
|
19670
19906
|
return false;
|
|
19671
19907
|
}
|
|
@@ -20066,9 +20302,9 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
|
|
|
20066
20302
|
delete step.httpRequest.openApi.definition;
|
|
20067
20303
|
} else if (openApiDefinitions.length > 0) {
|
|
20068
20304
|
findOperation: for (const openApiConfig of openApiDefinitions) {
|
|
20069
|
-
for (const
|
|
20070
|
-
for (const method in openApiConfig.definition.paths[
|
|
20071
|
-
if (openApiConfig.definition.paths[
|
|
20305
|
+
for (const path32 in openApiConfig.definition.paths) {
|
|
20306
|
+
for (const method in openApiConfig.definition.paths[path32]) {
|
|
20307
|
+
if (openApiConfig.definition.paths[path32][method].operationId === step.httpRequest.openApi.operationId) {
|
|
20072
20308
|
openApiDefinition = openApiConfig.definition;
|
|
20073
20309
|
step.httpRequest.openApi = {
|
|
20074
20310
|
...openApiConfig,
|
|
@@ -20138,10 +20374,7 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
|
|
|
20138
20374
|
};
|
|
20139
20375
|
}
|
|
20140
20376
|
if (step.httpRequest.openApi.statusCode) {
|
|
20141
|
-
step.httpRequest.statusCodes = [
|
|
20142
|
-
step.httpRequest.openApi.statusCode,
|
|
20143
|
-
...step.httpRequest.statusCodes || []
|
|
20144
|
-
];
|
|
20377
|
+
step.httpRequest.statusCodes = [step.httpRequest.openApi.statusCode].concat(step.httpRequest.statusCodes || []);
|
|
20145
20378
|
} else if (!step.httpRequest.statusCodes) {
|
|
20146
20379
|
step.httpRequest.statusCodes = Object.keys(operation.definition.responses).filter((code) => code.startsWith("2")).map((code) => Number(code));
|
|
20147
20380
|
}
|
|
@@ -20162,7 +20395,8 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
|
|
|
20162
20395
|
step.httpRequest = {
|
|
20163
20396
|
...step.httpRequest,
|
|
20164
20397
|
method: step.httpRequest.method || "get",
|
|
20165
|
-
|
|
20398
|
+
// A scalar statusCodes is one accepted code (as in checkLink).
|
|
20399
|
+
statusCodes: [].concat(step.httpRequest.statusCodes || [200, 201]),
|
|
20166
20400
|
request: step.httpRequest.request || {
|
|
20167
20401
|
params: {},
|
|
20168
20402
|
headers: {},
|
|
@@ -20426,8 +20660,8 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
|
|
|
20426
20660
|
result.description = descriptions.join(" ").trim();
|
|
20427
20661
|
return result;
|
|
20428
20662
|
}
|
|
20429
|
-
function fieldExistsAtPath(obj,
|
|
20430
|
-
const segments =
|
|
20663
|
+
function fieldExistsAtPath(obj, path32) {
|
|
20664
|
+
const segments = path32.match(/[^.[\]]+/g);
|
|
20431
20665
|
if (!segments) {
|
|
20432
20666
|
return false;
|
|
20433
20667
|
}
|
|
@@ -20585,6 +20819,11 @@ async function clickElement({ config, step, driver, appSession, processRegistry,
|
|
|
20585
20819
|
return result;
|
|
20586
20820
|
}
|
|
20587
20821
|
step = isValidStep.object;
|
|
20822
|
+
if (typeof step.click === "boolean") {
|
|
20823
|
+
result.status = "FAIL";
|
|
20824
|
+
result.description = `\`click\` needs an element, not ${step.click}. To click an element whose text is "${step.click}", quote it: "click": "${step.click}".`;
|
|
20825
|
+
return result;
|
|
20826
|
+
}
|
|
20588
20827
|
let findStep;
|
|
20589
20828
|
if (typeof step.click === "string") {
|
|
20590
20829
|
findStep = { find: step.click };
|
|
@@ -21619,6 +21858,7 @@ function raceBootInitiation({ startAcquire, onError }) {
|
|
|
21619
21858
|
|
|
21620
21859
|
// dist/core/tests.js
|
|
21621
21860
|
init_cacheDir();
|
|
21861
|
+
init_heavyDeps();
|
|
21622
21862
|
|
|
21623
21863
|
// dist/core/tests/runBrowserScript.js
|
|
21624
21864
|
init_validate();
|
|
@@ -22141,26 +22381,35 @@ async function uploadChangedFiles({ config, report, log: log3 }) {
|
|
|
22141
22381
|
for (const file of changedFiles) {
|
|
22142
22382
|
const uploader = getUploader(file.sourceIntegration);
|
|
22143
22383
|
if (!uploader) {
|
|
22144
|
-
|
|
22384
|
+
const why = `no uploader handles integration type \`${file.sourceIntegration.type}\`.`;
|
|
22385
|
+
log3(config, "warning", `Couldn't upload ${file.localPath}: ${why}`);
|
|
22145
22386
|
results.skipped++;
|
|
22146
22387
|
results.details.push({
|
|
22147
22388
|
localPath: file.localPath,
|
|
22148
22389
|
status: "SKIPPED",
|
|
22149
|
-
reason: `
|
|
22390
|
+
reason: `Skipped: ${why}`
|
|
22150
22391
|
});
|
|
22151
22392
|
continue;
|
|
22152
22393
|
}
|
|
22153
22394
|
const integrationConfig = getIntegrationConfig(config, file.sourceIntegration);
|
|
22154
22395
|
if (!integrationConfig) {
|
|
22155
|
-
|
|
22396
|
+
const why = `no \`${file.sourceIntegration.integrationName}\` integration is configured.`;
|
|
22397
|
+
log3(config, "warning", `Couldn't upload ${file.localPath}: ${why}`);
|
|
22156
22398
|
results.skipped++;
|
|
22157
22399
|
results.details.push({
|
|
22158
22400
|
localPath: file.localPath,
|
|
22159
22401
|
status: "SKIPPED",
|
|
22160
|
-
reason: `
|
|
22402
|
+
reason: `Skipped: ${why}`
|
|
22161
22403
|
});
|
|
22162
22404
|
continue;
|
|
22163
22405
|
}
|
|
22406
|
+
if (integrationConfig.uploadOnChange !== true) {
|
|
22407
|
+
const reason2 = `Skipped: \`uploadOnChange\` isn't enabled for integration ${file.sourceIntegration.integrationName}.`;
|
|
22408
|
+
log3(config, "debug", `${file.localPath}: ${reason2}`);
|
|
22409
|
+
results.skipped++;
|
|
22410
|
+
results.details.push({ localPath: file.localPath, status: "SKIPPED", reason: reason2 });
|
|
22411
|
+
continue;
|
|
22412
|
+
}
|
|
22164
22413
|
uploadTasks.push({
|
|
22165
22414
|
file,
|
|
22166
22415
|
uploader,
|
|
@@ -22232,8 +22481,6 @@ function getIntegrationConfig(config, sourceIntegration) {
|
|
|
22232
22481
|
// dist/core/tests.js
|
|
22233
22482
|
var import_node_http2 = __toESM(require("node:http"), 1);
|
|
22234
22483
|
var import_node_https3 = __toESM(require("node:https"), 1);
|
|
22235
|
-
var import_node_url5 = require("node:url");
|
|
22236
|
-
var __dirname4 = import_node_path30.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
|
|
22237
22484
|
var lastReuseConfig;
|
|
22238
22485
|
var appiumServerCache = createServerCache({
|
|
22239
22486
|
kill: (server) => killTree(server.process?.pid),
|
|
@@ -22794,6 +23041,8 @@ async function runViaApi({ resolvedTests, apiKey, config = {} }) {
|
|
|
22794
23041
|
steps: { pass: 0, fail: 0, warning: 0, skipped: 0 }
|
|
22795
23042
|
},
|
|
22796
23043
|
// Nothing ran, but the shape stays parity with runSpecs' short-circuit.
|
|
23044
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23045
|
+
environment: getEnvironment(),
|
|
22797
23046
|
durationMs: 0,
|
|
22798
23047
|
specs: []
|
|
22799
23048
|
};
|
|
@@ -22891,6 +23140,8 @@ async function runSpecs({ resolvedTests }) {
|
|
|
22891
23140
|
contexts: { pass: 0, fail: 0, warning: 0, skipped: 0 },
|
|
22892
23141
|
steps: { pass: 0, fail: 0, warning: 0, skipped: 0 }
|
|
22893
23142
|
},
|
|
23143
|
+
startedAt: new Date(runStart).toISOString(),
|
|
23144
|
+
environment: getEnvironment(),
|
|
22894
23145
|
durationMs: Date.now() - runStart,
|
|
22895
23146
|
specs: []
|
|
22896
23147
|
};
|
|
@@ -22917,6 +23168,10 @@ async function runSpecs({ resolvedTests }) {
|
|
|
22917
23168
|
const report = {
|
|
22918
23169
|
runId,
|
|
22919
23170
|
runDir,
|
|
23171
|
+
// Run metadata (ADR 01163): when execution started and the host that ran
|
|
23172
|
+
// it, so a consumer can tell product drift from environment drift.
|
|
23173
|
+
startedAt: new Date(runStart).toISOString(),
|
|
23174
|
+
environment: runnerDetails.environment,
|
|
22920
23175
|
summary: {
|
|
22921
23176
|
specs: {
|
|
22922
23177
|
pass: 0,
|
|
@@ -23391,30 +23646,31 @@ async function runSpecs({ resolvedTests }) {
|
|
|
23391
23646
|
process.off("SIGINT", onSignal);
|
|
23392
23647
|
process.off("SIGTERM", onSignal);
|
|
23393
23648
|
}
|
|
23394
|
-
const
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
// Default to true if not explicitly set to false
|
|
23398
|
-
);
|
|
23399
|
-
const globalUploadOnChange = config?.uploadOnChange ?? true;
|
|
23400
|
-
if (globalUploadOnChange && hasUploadEnabledIntegration && herettoConfigs.length > 0) {
|
|
23401
|
-
try {
|
|
23402
|
-
const uploadResults = await uploadChangedFiles({ config, report, log });
|
|
23403
|
-
report.uploadResults = uploadResults;
|
|
23404
|
-
} catch (error) {
|
|
23405
|
-
log(config, "warning", `Failed to upload changed files: ${error.message}`);
|
|
23406
|
-
report.uploadResults = {
|
|
23407
|
-
total: 0,
|
|
23408
|
-
successful: 0,
|
|
23409
|
-
failed: 0,
|
|
23410
|
-
skipped: 0,
|
|
23411
|
-
error: error.message
|
|
23412
|
-
};
|
|
23413
|
-
}
|
|
23414
|
-
}
|
|
23649
|
+
const uploadResults = await uploadChangedFilesIfEnabled({ config, report, log });
|
|
23650
|
+
if (uploadResults)
|
|
23651
|
+
report.uploadResults = uploadResults;
|
|
23415
23652
|
report.durationMs = Date.now() - runStart;
|
|
23416
23653
|
return report;
|
|
23417
23654
|
}
|
|
23655
|
+
async function uploadChangedFilesIfEnabled({ config, report, log: logFn, upload = uploadChangedFiles }) {
|
|
23656
|
+
const integrationEntries = Object.values(config?.integrations ?? {}).flatMap((entries) => Array.isArray(entries) ? entries : []);
|
|
23657
|
+
if (!integrationEntries.some((entry) => entry?.uploadOnChange === true)) {
|
|
23658
|
+
return void 0;
|
|
23659
|
+
}
|
|
23660
|
+
try {
|
|
23661
|
+
return await upload({ config, report, log: logFn });
|
|
23662
|
+
} catch (error) {
|
|
23663
|
+
logFn(config, "warning", `Failed to upload changed files: ${error.message}`);
|
|
23664
|
+
return {
|
|
23665
|
+
total: 0,
|
|
23666
|
+
successful: 0,
|
|
23667
|
+
failed: 0,
|
|
23668
|
+
skipped: 0,
|
|
23669
|
+
details: [],
|
|
23670
|
+
error: error.message
|
|
23671
|
+
};
|
|
23672
|
+
}
|
|
23673
|
+
}
|
|
23418
23674
|
async function runRoutedSpec({ spec, specReport, specGuardSkip, config, runnerDetails, appiumPool, portToDisplay, metaValues, installAttempts, warmUpResults, processRegistry, deviceRegistry, simulatorRegistry, appServerPool, platform, markAutoRecord, limit, resourceRegistry, exclusivityCtx }) {
|
|
23419
23675
|
let stopRest = false;
|
|
23420
23676
|
let warnedRunStop = false;
|
|
@@ -24437,6 +24693,14 @@ ${JSON.stringify(context, null, 2)}`);
|
|
|
24437
24693
|
engine: String(mobileWebBrowserName).toLowerCase(),
|
|
24438
24694
|
driver
|
|
24439
24695
|
});
|
|
24696
|
+
const versions = sessionVersions({ capabilities: driver?.capabilities });
|
|
24697
|
+
if (versions.browserVersion)
|
|
24698
|
+
contextReport.browser = {
|
|
24699
|
+
...contextReport.browser,
|
|
24700
|
+
version: versions.browserVersion
|
|
24701
|
+
};
|
|
24702
|
+
if (versions.driver)
|
|
24703
|
+
contextReport.driver = versions.driver;
|
|
24440
24704
|
} else if (driverRequired) {
|
|
24441
24705
|
appiumPort = await appiumPool.acquire();
|
|
24442
24706
|
if (portToDisplay) {
|
|
@@ -24566,6 +24830,17 @@ ${JSON.stringify(context, null, 2)}`);
|
|
|
24566
24830
|
};
|
|
24567
24831
|
clog("warning", fellBackNote);
|
|
24568
24832
|
}
|
|
24833
|
+
const versions = sessionVersions({
|
|
24834
|
+
capabilities: driver?.capabilities,
|
|
24835
|
+
app: availableApps.find((app) => app.name === normalizeBrowserName(startedName))
|
|
24836
|
+
});
|
|
24837
|
+
if (versions.browserVersion)
|
|
24838
|
+
contextReport.browser = {
|
|
24839
|
+
...contextReport.browser,
|
|
24840
|
+
version: versions.browserVersion
|
|
24841
|
+
};
|
|
24842
|
+
if (versions.driver)
|
|
24843
|
+
contextReport.driver = versions.driver;
|
|
24569
24844
|
if (driver) {
|
|
24570
24845
|
browserSessions = createSessionRegistry({
|
|
24571
24846
|
open: async (engine, overrides) => {
|
|
@@ -24748,7 +25023,12 @@ ${JSON.stringify(r, null, 2)}`);
|
|
|
24748
25023
|
r.resultDescription = r.description;
|
|
24749
25024
|
delete r.status;
|
|
24750
25025
|
delete r.description;
|
|
24751
|
-
return {
|
|
25026
|
+
return {
|
|
25027
|
+
...step,
|
|
25028
|
+
...r,
|
|
25029
|
+
startedAt: new Date(stepStart).toISOString(),
|
|
25030
|
+
durationMs: Date.now() - stepStart
|
|
25031
|
+
};
|
|
24752
25032
|
};
|
|
24753
25033
|
let stepReport = await runStepOnce();
|
|
24754
25034
|
let attempts = 1;
|
|
@@ -24952,6 +25232,7 @@ async function runContextWithRetries(args, runContextFn = runContext, delayMs =
|
|
|
24952
25232
|
const start = Date.now();
|
|
24953
25233
|
const report2 = await runContextFn(a);
|
|
24954
25234
|
if (report2 && typeof report2 === "object") {
|
|
25235
|
+
report2.startedAt = new Date(start).toISOString();
|
|
24955
25236
|
report2.durationMs = Date.now() - start;
|
|
24956
25237
|
}
|
|
24957
25238
|
return report2;
|
|
@@ -25024,6 +25305,7 @@ async function stopAllRecordings({ config, context, driver, contextReport }) {
|
|
|
25024
25305
|
contextReport.steps.push({
|
|
25025
25306
|
...stopRecordStep,
|
|
25026
25307
|
...stepResult,
|
|
25308
|
+
startedAt: new Date(stopStart).toISOString(),
|
|
25027
25309
|
durationMs: Date.now() - stopStart
|
|
25028
25310
|
});
|
|
25029
25311
|
} catch (error) {
|
|
@@ -25033,6 +25315,7 @@ async function stopAllRecordings({ config, context, driver, contextReport }) {
|
|
|
25033
25315
|
...stopRecordStep,
|
|
25034
25316
|
result: "FAIL",
|
|
25035
25317
|
resultDescription: `Couldn't stop recording. ${error?.message ?? error}`,
|
|
25318
|
+
startedAt: new Date(stopStart).toISOString(),
|
|
25036
25319
|
durationMs: Date.now() - stopStart
|
|
25037
25320
|
});
|
|
25038
25321
|
}
|
|
@@ -25314,7 +25597,7 @@ async function spawnAppiumServer(appiumEntry, config, display, extraEnv, extraAr
|
|
|
25314
25597
|
...extraArgs ?? []
|
|
25315
25598
|
], {
|
|
25316
25599
|
windowsHide: true,
|
|
25317
|
-
cwd:
|
|
25600
|
+
cwd: packageRoot(),
|
|
25318
25601
|
env
|
|
25319
25602
|
});
|
|
25320
25603
|
proc.on("error", (err) => {
|
|
@@ -25399,8 +25682,8 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
|
25399
25682
|
if (!isRetryableSessionError(String(err && err.message), startupCeiling))
|
|
25400
25683
|
throw err;
|
|
25401
25684
|
if (attempt < maxAttempts) {
|
|
25402
|
-
const
|
|
25403
|
-
ctx.log?.(`Session start attempt ${attempt} of ${maxAttempts} failed (${
|
|
25685
|
+
const reason2 = String(err?.message ?? err).split("\n")[0];
|
|
25686
|
+
ctx.log?.(`Session start attempt ${attempt} of ${maxAttempts} failed (${reason2}); retrying.`);
|
|
25404
25687
|
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
25405
25688
|
}
|
|
25406
25689
|
}
|
|
@@ -25495,7 +25778,7 @@ async function getRunner(options = {}) {
|
|
|
25495
25778
|
...useDriversArgs(process.env.APPIUM_HOME, browserPoolDriverPackages(process.platform))
|
|
25496
25779
|
], {
|
|
25497
25780
|
windowsHide: true,
|
|
25498
|
-
cwd:
|
|
25781
|
+
cwd: packageRoot()
|
|
25499
25782
|
});
|
|
25500
25783
|
appium.on("error", (err) => {
|
|
25501
25784
|
log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
|
|
@@ -25560,6 +25843,165 @@ async function getRunner(options = {}) {
|
|
|
25560
25843
|
// dist/core/index.js
|
|
25561
25844
|
init_telemLite();
|
|
25562
25845
|
init_files();
|
|
25846
|
+
|
|
25847
|
+
// dist/upgradeNotice.js
|
|
25848
|
+
var import_node_path31 = __toESM(require("node:path"), 1);
|
|
25849
|
+
var UPGRADE_GUIDE_URL = "https://docs.doc-detective.com/docs/ci/upgrade-to-v5";
|
|
25850
|
+
var warned = /* @__PURE__ */ new Set();
|
|
25851
|
+
function isAction() {
|
|
25852
|
+
try {
|
|
25853
|
+
return JSON.parse(process.env.DOC_DETECTIVE_META || "{}").dist_interface === "github-actions";
|
|
25854
|
+
} catch {
|
|
25855
|
+
return false;
|
|
25856
|
+
}
|
|
25857
|
+
}
|
|
25858
|
+
function warnUpgrade(messages, logLevel) {
|
|
25859
|
+
if (logLevel === "silent" || isAction())
|
|
25860
|
+
return;
|
|
25861
|
+
for (const message of messages) {
|
|
25862
|
+
if (warned.has(message))
|
|
25863
|
+
continue;
|
|
25864
|
+
warned.add(message);
|
|
25865
|
+
console.error(`Warning: ${message} See ${UPGRADE_GUIDE_URL}`);
|
|
25866
|
+
}
|
|
25867
|
+
}
|
|
25868
|
+
function legacyEnv(env) {
|
|
25869
|
+
try {
|
|
25870
|
+
const parsed = JSON.parse(env.DOC_DETECTIVE || "{}");
|
|
25871
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
25872
|
+
} catch {
|
|
25873
|
+
return {};
|
|
25874
|
+
}
|
|
25875
|
+
}
|
|
25876
|
+
var ROUTING = ["onPass", "onFail", "onWarning", "onSkip"];
|
|
25877
|
+
var LOCATORS = ["selector", "elementText", "elementId", "elementTestId", "elementClass", "elementAttribute", "elementAria"];
|
|
25878
|
+
var REJECTS = "Doc Detective 5.0 rejects with exit 2";
|
|
25879
|
+
var EXIT_2 = `which ${REJECTS}`;
|
|
25880
|
+
function routes(owner) {
|
|
25881
|
+
return ROUTING.flatMap((key) => Array.isArray(owner[key]) ? owner[key] : []);
|
|
25882
|
+
}
|
|
25883
|
+
function testWarnings(test, where) {
|
|
25884
|
+
return routes(test).flatMap((route) => [
|
|
25885
|
+
...route.stop === "run" ? [`A test in ${where} has \`stop: "run"\` in its routing, ${EXIT_2}. Use \`stop: "spec"\`, which is what 4.x does.`] : [],
|
|
25886
|
+
...["retry", "goToStep"].filter((key) => key in route).map((key) => `A test in ${where} has \`${key}\` in its routing, which 4.x ignores and ${REJECTS}. Remove it, or move it to a step.`)
|
|
25887
|
+
]);
|
|
25888
|
+
}
|
|
25889
|
+
var FREE_FORM = /* @__PURE__ */ new Set([
|
|
25890
|
+
"request",
|
|
25891
|
+
"response",
|
|
25892
|
+
"headers",
|
|
25893
|
+
"body",
|
|
25894
|
+
"params",
|
|
25895
|
+
"parameters",
|
|
25896
|
+
"elementAttribute",
|
|
25897
|
+
"variables",
|
|
25898
|
+
"outputs",
|
|
25899
|
+
"env",
|
|
25900
|
+
"driverOptions",
|
|
25901
|
+
"openApi"
|
|
25902
|
+
]);
|
|
25903
|
+
function negatives(value, found = /* @__PURE__ */ new Set()) {
|
|
25904
|
+
if (value && typeof value === "object") {
|
|
25905
|
+
for (const [key, child] of Object.entries(value)) {
|
|
25906
|
+
if (FREE_FORM.has(key))
|
|
25907
|
+
continue;
|
|
25908
|
+
if (typeof child === "number" && child < 0 && (key === "wait" || /(timeout|duration|delay)$/i.test(key)))
|
|
25909
|
+
found.add(key);
|
|
25910
|
+
negatives(child, found);
|
|
25911
|
+
}
|
|
25912
|
+
}
|
|
25913
|
+
return found;
|
|
25914
|
+
}
|
|
25915
|
+
function offend(offenders, row, value, message) {
|
|
25916
|
+
if (!offenders.has(row))
|
|
25917
|
+
offenders.set(row, { values: /* @__PURE__ */ new Set(), message });
|
|
25918
|
+
offenders.get(row).values.add(value);
|
|
25919
|
+
}
|
|
25920
|
+
function stepWarnings(step, where, offenders) {
|
|
25921
|
+
const messages = [];
|
|
25922
|
+
for (const route of routes(step)) {
|
|
25923
|
+
if (route.stop === "spec" || route.stop === "run") {
|
|
25924
|
+
messages.push(`A step in ${where} has \`stop: "${route.stop}"\` in its routing, ${EXIT_2}. Use \`stop: "test"\` on the step and \`stop: "spec"\` on the test, which works today.`);
|
|
25925
|
+
}
|
|
25926
|
+
if ("goToTest" in route) {
|
|
25927
|
+
messages.push(`A step in ${where} has \`goToTest\` in its routing, which 4.x ignores and ${REJECTS}. Move it to the test's routing, which works today.`);
|
|
25928
|
+
}
|
|
25929
|
+
}
|
|
25930
|
+
if (typeof step.click === "boolean") {
|
|
25931
|
+
messages.push(`A \`click\` step in ${where} is \`${step.click}\`, which 4.x reads as text to click and ${REJECTS}. To click an element whose text is '${step.click}', quote it as \`"click": "${step.click}"\`, which works today. If you don't want a click, remove the step.`);
|
|
25932
|
+
}
|
|
25933
|
+
const objects = (value) => [value].flat().filter((s) => s && typeof s === "object");
|
|
25934
|
+
if (objects(step.startSurface).concat(objects(step.closeSurface)).some((s) => s.browser === "edge")) {
|
|
25935
|
+
messages.push(`A surface in ${where} uses \`browser: "edge"\`, which Doc Detective 5.0 removes. Use another \`browser\`, such as \`chrome\`, which works today.`);
|
|
25936
|
+
}
|
|
25937
|
+
const starts = objects(step.startSurface);
|
|
25938
|
+
for (const surface of starts) {
|
|
25939
|
+
const device = surface.device && typeof surface.device === "object" ? surface.device : {};
|
|
25940
|
+
const removed = ["orientation", "udid", "provider"].filter((key) => key in device);
|
|
25941
|
+
if (removed.length) {
|
|
25942
|
+
messages.push(`A \`startSurface\` device in ${where} sets ${removed.map((key) => `\`${key}\``).join(", ")}, which Doc Detective 5.0 removes. Remove ${removed.length > 1 ? "them" : "it"}, which works today.`);
|
|
25943
|
+
}
|
|
25944
|
+
if (surface.app != null && surface.workingDirectory === ".") {
|
|
25945
|
+
messages.push(`An app in ${where} sets \`workingDirectory: "."\`, which 4.x tolerates and Doc Detective 5.0 fails. Remove it, which works today.`);
|
|
25946
|
+
}
|
|
25947
|
+
}
|
|
25948
|
+
const zeroTimeouts = [
|
|
25949
|
+
...["runShell", "runCode", "goTo", "httpRequest"].filter((action) => step[action]?.timeout === 0),
|
|
25950
|
+
...starts.some((s) => s.timeout === 0) ? ["startSurface"] : []
|
|
25951
|
+
];
|
|
25952
|
+
for (const action of zeroTimeouts) {
|
|
25953
|
+
messages.push(`A \`${action}\` step in ${where} sets \`timeout: 0\`, ${EXIT_2} on \`runShell\`, \`runCode\`, \`goTo\`, \`httpRequest\`, and \`startSurface\` processes and apps. Remove \`timeout\` for the default, or set it to 1 or more, which works today. Locator and wait timeouts can still be 0.`);
|
|
25954
|
+
}
|
|
25955
|
+
for (const key of negatives(step)) {
|
|
25956
|
+
messages.push(`A step in ${where} has a negative \`${key}\`, ${EXIT_2}. Use a positive value, which works today.`);
|
|
25957
|
+
}
|
|
25958
|
+
if (step.screenshot === false) {
|
|
25959
|
+
messages.push(`A \`screenshot: false\` step in ${where} takes a screenshot today, and Doc Detective 5.0 skips it. To keep the screenshot, use \`screenshot: true\`, which works today.`);
|
|
25960
|
+
}
|
|
25961
|
+
const find = step.find && typeof step.find === "object" ? step.find : {};
|
|
25962
|
+
const click = find.click;
|
|
25963
|
+
if (typeof click === "string" && !["left", "right", "middle"].includes(click) || click && typeof click === "object" && Object.keys(click).some((key) => key !== "button" && key !== "duration")) {
|
|
25964
|
+
messages.push(`A \`find.click\` in ${where} isn't \`true\`, \`false\`, a button, or \`{button, duration}\`, ${EXIT_2}. Use \`click: true\` to click the found element, or a separate \`click\` step, which works today.`);
|
|
25965
|
+
}
|
|
25966
|
+
if (find.type && typeof find.type === "object" && LOCATORS.some((key) => key in find.type)) {
|
|
25967
|
+
messages.push(`A \`find.type\` in ${where} sets its own locator, ${EXIT_2}. Move it to a separate \`type\` step, which works today.`);
|
|
25968
|
+
}
|
|
25969
|
+
for (const action of ["goTo", "httpRequest"]) {
|
|
25970
|
+
const url = typeof step[action] === "string" ? step[action] : step[action]?.url;
|
|
25971
|
+
if (typeof url === "string" && !/^(https?:\/\/|\/)/.test(url) && !/^\$[A-Za-z0-9_]+$/.test(url)) {
|
|
25972
|
+
offend(offenders, `${action} URL`, url, (first) => `A \`${action}\` URL in ${where} (${first}) doesn't start with \`http://\`, \`https://\`, or \`/\`, and isn't a whole \`$VAR\`, ${EXIT_2}. Put the whole URL in one variable, which works today.`);
|
|
25973
|
+
}
|
|
25974
|
+
}
|
|
25975
|
+
for (const action of ["saveCookie", "loadCookie"]) {
|
|
25976
|
+
const value = step[action];
|
|
25977
|
+
if (typeof value === "string" && value.includes("/") && !value.endsWith(".txt")) {
|
|
25978
|
+
offend(offenders, action, value, (first) => `A \`${action}\` in ${where} (${first}) has a \`/\` but doesn't end in \`.txt\`, ${EXIT_2}. End a file path in \`.txt\`, which works today.`);
|
|
25979
|
+
}
|
|
25980
|
+
}
|
|
25981
|
+
return messages;
|
|
25982
|
+
}
|
|
25983
|
+
function specWarnings(resolvedTests) {
|
|
25984
|
+
const messages = [];
|
|
25985
|
+
for (const spec of resolvedTests?.specs ?? []) {
|
|
25986
|
+
const file = spec.contentPath;
|
|
25987
|
+
const where = typeof file === "string" ? `'${import_node_path31.default.isAbsolute(file) ? import_node_path31.default.relative(process.cwd(), file) : file}'` : "a spec";
|
|
25988
|
+
const offenders = /* @__PURE__ */ new Map();
|
|
25989
|
+
for (const test of spec.tests ?? []) {
|
|
25990
|
+
messages.push(...testWarnings(test, where));
|
|
25991
|
+
for (const context of test.contexts ?? []) {
|
|
25992
|
+
for (const step of context.steps ?? [])
|
|
25993
|
+
messages.push(...stepWarnings(step, where, offenders));
|
|
25994
|
+
}
|
|
25995
|
+
}
|
|
25996
|
+
for (const { values, message } of offenders.values()) {
|
|
25997
|
+
const [first] = values;
|
|
25998
|
+
messages.push(message(values.size > 1 ? `\`${first}\` and ${values.size - 1} more` : `\`${first}\``));
|
|
25999
|
+
}
|
|
26000
|
+
}
|
|
26001
|
+
return [...new Set(messages)];
|
|
26002
|
+
}
|
|
26003
|
+
|
|
26004
|
+
// dist/core/index.js
|
|
25563
26005
|
var supportMessage = `
|
|
25564
26006
|
##########################################################################
|
|
25565
26007
|
# Thanks for using Doc Detective! If this project was helpful to you, #
|
|
@@ -25604,6 +26046,7 @@ async function runTestsAndReport(config, options) {
|
|
|
25604
26046
|
config = { ...resolvedTests.config || {}, ...config || {} };
|
|
25605
26047
|
resolvedTests.config = config;
|
|
25606
26048
|
}
|
|
26049
|
+
const callerLogLevel = config?.logLevel;
|
|
25607
26050
|
if (config.dryRun) {
|
|
25608
26051
|
config.logLevel = "silent";
|
|
25609
26052
|
if (resolvedTests)
|
|
@@ -25635,6 +26078,12 @@ async function runTestsAndReport(config, options) {
|
|
|
25635
26078
|
log(config, "warning", "Couldn't resolve any tests.");
|
|
25636
26079
|
return null;
|
|
25637
26080
|
}
|
|
26081
|
+
const upgradeLogLevel = config.dryRun ? legacyEnv(process.env).config?.logLevel ?? callerLogLevel : effectiveConfig.logLevel;
|
|
26082
|
+
try {
|
|
26083
|
+
warnUpgrade(specWarnings(resolvedTests), upgradeLogLevel);
|
|
26084
|
+
} catch (error) {
|
|
26085
|
+
log(effectiveConfig, "debug", `Couldn't check specs for Doc Detective 5.0 changes: ${error?.message || error}`);
|
|
26086
|
+
}
|
|
25638
26087
|
const resolvedConfig = resolvedTests.config;
|
|
25639
26088
|
if (!resolvedConfig.dryRun && (willRunViaApi || runNeedsAvailableApps(resolvedTests.specs))) {
|
|
25640
26089
|
resolvedConfig.environment.apps = await getAvailableApps({ config: resolvedConfig });
|