doc-detective 4.6.1 → 4.7.0-runtime-dependency-detection.1

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.
Files changed (66) hide show
  1. package/dist/cli.js +57 -6
  2. package/dist/cli.js.map +1 -1
  3. package/dist/common/src/schemas/schemas.json +6 -18
  4. package/dist/common/src/types/generated/config_v3.d.ts +2 -1
  5. package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
  6. package/dist/common/src/types/generated/resolvedTests_v3.d.ts +2 -1
  7. package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
  8. package/dist/core/config.d.ts +17 -1
  9. package/dist/core/config.d.ts.map +1 -1
  10. package/dist/core/config.js +218 -63
  11. package/dist/core/config.js.map +1 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +7 -10
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/tests.d.ts +41 -1
  16. package/dist/core/tests.d.ts.map +1 -1
  17. package/dist/core/tests.js +166 -7
  18. package/dist/core/tests.js.map +1 -1
  19. package/dist/debug/command.d.ts +10 -0
  20. package/dist/debug/command.d.ts.map +1 -0
  21. package/dist/debug/command.js +72 -0
  22. package/dist/debug/command.js.map +1 -0
  23. package/dist/debug/envvars.d.ts +9 -0
  24. package/dist/debug/envvars.d.ts.map +1 -0
  25. package/dist/debug/envvars.js +231 -0
  26. package/dist/debug/envvars.js.map +1 -0
  27. package/dist/debug/index.d.ts +85 -0
  28. package/dist/debug/index.d.ts.map +1 -0
  29. package/dist/debug/index.js +465 -0
  30. package/dist/debug/index.js.map +1 -0
  31. package/dist/debug/redact.d.ts +7 -0
  32. package/dist/debug/redact.d.ts.map +1 -0
  33. package/dist/debug/redact.js +190 -0
  34. package/dist/debug/redact.js.map +1 -0
  35. package/dist/debug/render.d.ts +8 -0
  36. package/dist/debug/render.d.ts.map +1 -0
  37. package/dist/debug/render.js +42 -0
  38. package/dist/debug/render.js.map +1 -0
  39. package/dist/debug/system.d.ts +24 -0
  40. package/dist/debug/system.d.ts.map +1 -0
  41. package/dist/debug/system.js +59 -0
  42. package/dist/debug/system.js.map +1 -0
  43. package/dist/debug/tools.d.ts +11 -0
  44. package/dist/debug/tools.d.ts.map +1 -0
  45. package/dist/debug/tools.js +170 -0
  46. package/dist/debug/tools.js.map +1 -0
  47. package/dist/hints/hints.d.ts.map +1 -1
  48. package/dist/hints/hints.js +17 -0
  49. package/dist/hints/hints.js.map +1 -1
  50. package/dist/index.cjs +374 -147
  51. package/dist/runtime/browsers.d.ts +14 -0
  52. package/dist/runtime/browsers.d.ts.map +1 -1
  53. package/dist/runtime/browsers.js +23 -0
  54. package/dist/runtime/browsers.js.map +1 -1
  55. package/dist/runtime/installer.d.ts.map +1 -1
  56. package/dist/runtime/installer.js +44 -9
  57. package/dist/runtime/installer.js.map +1 -1
  58. package/dist/runtime/loader.d.ts +20 -0
  59. package/dist/runtime/loader.d.ts.map +1 -1
  60. package/dist/runtime/loader.js +50 -0
  61. package/dist/runtime/loader.js.map +1 -1
  62. package/dist/utils.d.ts +2 -1
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/utils.js +38 -11
  65. package/dist/utils.js.map +1 -1
  66. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -9923,7 +9923,8 @@ var init_schemas = __esm({
9923
9923
  title: "Environment details"
9924
9924
  },
9925
9925
  debug: {
9926
- description: "Enable debugging mode. `true` allows pausing on breakpoints, waiting for user input before continuing. `stepThrough` pauses at every step, waiting for user input before continuing. `false` disables all debugging.",
9926
+ description: "Deprecated and ignored. Previously reserved for an interactive step-through debugger that was never implemented. Retained so existing configs continue to validate. For diagnostics, run `doc-detective debug` or set the `DOC_DETECTIVE_DEBUG` environment variable.",
9927
+ deprecated: true,
9927
9928
  anyOf: [
9928
9929
  {
9929
9930
  type: "boolean"
@@ -9934,8 +9935,7 @@ var init_schemas = __esm({
9934
9935
  "stepThrough"
9935
9936
  ]
9936
9937
  }
9937
- ],
9938
- default: false
9938
+ ]
9939
9939
  },
9940
9940
  dryRun: {
9941
9941
  description: "If `true`, fully resolve tests (file/env config merge, schema validation, file detection, inline-test extraction) and emit the resolved test plan as JSON, but do not execute any steps. Equivalent to `--dry-run` on the CLI.",
@@ -18245,12 +18245,6 @@ var init_schemas = __esm({
18245
18245
  {
18246
18246
  debug: false
18247
18247
  },
18248
- {
18249
- debug: true
18250
- },
18251
- {
18252
- debug: "stepThrough"
18253
- },
18254
18248
  {
18255
18249
  integrations: {
18256
18250
  docDetectiveApi: {
@@ -31882,7 +31876,8 @@ var init_schemas = __esm({
31882
31876
  title: "Environment details"
31883
31877
  },
31884
31878
  debug: {
31885
- description: "Enable debugging mode. `true` allows pausing on breakpoints, waiting for user input before continuing. `stepThrough` pauses at every step, waiting for user input before continuing. `false` disables all debugging.",
31879
+ description: "Deprecated and ignored. Previously reserved for an interactive step-through debugger that was never implemented. Retained so existing configs continue to validate. For diagnostics, run `doc-detective debug` or set the `DOC_DETECTIVE_DEBUG` environment variable.",
31880
+ deprecated: true,
31886
31881
  anyOf: [
31887
31882
  {
31888
31883
  type: "boolean"
@@ -31893,8 +31888,7 @@ var init_schemas = __esm({
31893
31888
  "stepThrough"
31894
31889
  ]
31895
31890
  }
31896
- ],
31897
- default: false
31891
+ ]
31898
31892
  },
31899
31893
  dryRun: {
31900
31894
  description: "If `true`, fully resolve tests (file/env config merge, schema validation, file detection, inline-test extraction) and emit the resolved test plan as JSON, but do not execute any steps. Equivalent to `--dry-run` on the CLI.",
@@ -40204,12 +40198,6 @@ var init_schemas = __esm({
40204
40198
  {
40205
40199
  debug: false
40206
40200
  },
40207
- {
40208
- debug: true
40209
- },
40210
- {
40211
- debug: "stepThrough"
40212
- },
40213
40201
  {
40214
40202
  integrations: {
40215
40203
  docDetectiveApi: {
@@ -130740,7 +130728,9 @@ var loader_exports = {};
130740
130728
  __export(loader_exports, {
130741
130729
  ensureRuntimeInstalled: () => ensureRuntimeInstalled,
130742
130730
  loadHeavyDep: () => loadHeavyDep,
130743
- resolveHeavyDepPath: () => resolveHeavyDepPath
130731
+ resolveHeavyDepPath: () => resolveHeavyDepPath,
130732
+ resolveHeavyDepSource: () => resolveHeavyDepSource,
130733
+ resolveHeavyDepVersion: () => resolveHeavyDepVersion
130744
130734
  });
130745
130735
  function tryResolveFromShim(name) {
130746
130736
  try {
@@ -130764,6 +130754,36 @@ function tryResolveFromCache(name, ctx = {}) {
130764
130754
  function resolveHeavyDepPath(name, ctx = {}) {
130765
130755
  return tryResolveFromShim(name) ?? tryResolveFromCache(name, ctx);
130766
130756
  }
130757
+ function resolveHeavyDepSource(name, ctx = {}) {
130758
+ if (tryResolveFromShim(name))
130759
+ return "shim";
130760
+ if (tryResolveFromCache(name, ctx))
130761
+ return "cache";
130762
+ return null;
130763
+ }
130764
+ function resolveHeavyDepVersion(name, ctx = {}) {
130765
+ const entry = resolveHeavyDepPath(name, ctx);
130766
+ if (!entry)
130767
+ return null;
130768
+ let dir = import_node_path4.default.dirname(entry);
130769
+ for (let i = 0; i < 12; i++) {
130770
+ const pkgJsonPath = import_node_path4.default.join(dir, "package.json");
130771
+ try {
130772
+ if (import_node_fs4.default.existsSync(pkgJsonPath)) {
130773
+ const parsed = JSON.parse(import_node_fs4.default.readFileSync(pkgJsonPath, "utf8"));
130774
+ if (parsed?.name === name && typeof parsed.version === "string") {
130775
+ return parsed.version;
130776
+ }
130777
+ }
130778
+ } catch {
130779
+ }
130780
+ const parent = import_node_path4.default.dirname(dir);
130781
+ if (parent === dir)
130782
+ break;
130783
+ dir = parent;
130784
+ }
130785
+ return null;
130786
+ }
130767
130787
  async function loadHeavyDep(name, options = {}) {
130768
130788
  const { autoInstall = true, ctx = {}, deps = {} } = options;
130769
130789
  let resolved = tryResolveFromShim(name);
@@ -131426,6 +131446,7 @@ var config_exports = {};
131426
131446
  __export(config_exports, {
131427
131447
  clearAppCache: () => clearAppCache,
131428
131448
  getAvailableApps: () => getAvailableApps,
131449
+ getBrowserDiagnostics: () => getBrowserDiagnostics,
131429
131450
  getEnvironment: () => getEnvironment,
131430
131451
  resolveConcurrentRunners: () => resolveConcurrentRunners,
131431
131452
  setConfig: () => setConfig
@@ -131619,19 +131640,14 @@ function clearAppCache(config) {
131619
131640
  }
131620
131641
  cachedAppsByDir.delete(cacheKeyFor(config));
131621
131642
  }
131622
- async function getAvailableApps({ config }) {
131623
- const browsersDir = getBrowsersDir({ cacheDir: config?.cacheDir });
131624
- const key = browsersDir;
131625
- const hit = cachedAppsByDir.get(key);
131626
- if (hit)
131627
- return hit;
131643
+ async function probeBrowserEnvironment({ config, browsersDir }) {
131628
131644
  setAppiumHome({ cacheDir: config?.cacheDir });
131629
131645
  const cwd = process.cwd();
131630
131646
  process.chdir(import_node_path7.default.join(__dirname4, "../.."));
131631
- const apps = [];
131647
+ let installedBrowsers = [];
131632
131648
  let browserDetectionFailed = false;
131649
+ let appiumDriverOutput = "";
131633
131650
  try {
131634
- let installedBrowsers = [];
131635
131651
  const browsersInstalled = resolveHeavyDepPath("@puppeteer/browsers", {
131636
131652
  cacheDir: config?.cacheDir
131637
131653
  });
@@ -131686,47 +131702,165 @@ async function getAvailableApps({ config }) {
131686
131702
  });
131687
131703
  });
131688
131704
  });
131689
- const appiumDriverOutput = installedAppiumDrivers.stdout + "\n" + installedAppiumDrivers.stderr;
131690
- const chrome = installedBrowsers.find((browser) => browser.browser === "chrome");
131691
- const chromeVersion = chrome?.buildId;
131692
- const chromedriver = installedBrowsers.find((browser) => browser.browser === "chromedriver");
131693
- const appiumChromium = appiumDriverOutput.match(/\n.*chromium.*installed \(npm\).*\n/);
131694
- if (chrome && chromedriver && appiumChromium) {
131695
- apps.push({
131696
- name: "chrome",
131697
- version: chromeVersion,
131698
- path: chrome.executablePath,
131699
- driver: chromedriver.executablePath
131700
- });
131701
- }
131702
- const firefox = installedBrowsers.find((browser) => browser.browser === "firefox");
131703
- const appiumFirefox = appiumDriverOutput.match(/\n.*gecko.*installed \(npm\).*\n/);
131704
- if (firefox && appiumFirefox) {
131705
- apps.push({
131706
- name: "firefox",
131707
- version: firefox.buildId,
131708
- path: firefox.executablePath
131709
- });
131710
- }
131711
- if (config.environment.platform === "mac") {
131712
- const safariVersion = await spawnCommand("defaults read /Applications/Safari.app/Contents/Info.plist CFBundleShortVersionString");
131713
- const appiumSafari = appiumDriverOutput.match(/\n.*safari.*installed \(npm\).*\n/);
131714
- if (safariVersion.exitCode === 0 && appiumSafari) {
131715
- apps.push({ name: "safari", version: safariVersion.stdout.trim(), path: "" });
131716
- }
131717
- }
131705
+ appiumDriverOutput = installedAppiumDrivers.stdout + "\n" + installedAppiumDrivers.stderr;
131718
131706
  } finally {
131719
131707
  process.chdir(cwd);
131720
131708
  }
131709
+ return { installedBrowsers, appiumDriverOutput, browserDetectionFailed };
131710
+ }
131711
+ async function getAvailableApps({ config }) {
131712
+ const browsersDir = getBrowsersDir({ cacheDir: config?.cacheDir });
131713
+ const key = browsersDir;
131714
+ const hit = cachedAppsByDir.get(key);
131715
+ if (hit)
131716
+ return hit;
131717
+ const { installedBrowsers, appiumDriverOutput, browserDetectionFailed } = await probeBrowserEnvironment({ config, browsersDir });
131718
+ const apps = [];
131719
+ const chrome = installedBrowsers.find((browser) => browser.browser === "chrome");
131720
+ const chromeVersion = chrome?.buildId;
131721
+ const chromedriver = installedBrowsers.find((browser) => browser.browser === "chromedriver");
131722
+ const appiumChromium = appiumDriverOutput.match(/\n.*chromium.*installed \(npm\).*\n/);
131723
+ if (chrome && chromedriver && appiumChromium) {
131724
+ apps.push({
131725
+ name: "chrome",
131726
+ version: chromeVersion,
131727
+ path: chrome.executablePath,
131728
+ driver: chromedriver.executablePath
131729
+ });
131730
+ }
131731
+ const firefox = installedBrowsers.find((browser) => browser.browser === "firefox");
131732
+ const appiumFirefox = appiumDriverOutput.match(/\n.*gecko.*installed \(npm\).*\n/);
131733
+ if (firefox && appiumFirefox) {
131734
+ apps.push({
131735
+ name: "firefox",
131736
+ version: firefox.buildId,
131737
+ path: firefox.executablePath
131738
+ });
131739
+ }
131740
+ if (config.environment.platform === "mac") {
131741
+ const safariVersion = await spawnCommand("defaults read /Applications/Safari.app/Contents/Info.plist CFBundleShortVersionString");
131742
+ const appiumSafari = appiumDriverOutput.match(/\n.*safari.*installed \(npm\).*\n/);
131743
+ if (safariVersion.exitCode === 0 && appiumSafari) {
131744
+ apps.push({ name: "safari", version: safariVersion.stdout.trim(), path: "" });
131745
+ }
131746
+ }
131721
131747
  if (!browserDetectionFailed)
131722
131748
  cachedAppsByDir.set(key, apps);
131723
131749
  return apps;
131724
131750
  }
131725
- var import_node_os3, import_node_path7, import_node_child_process3, import_node_url5, __dirname4, platformMap, defaultFileTypes, cachedAppsByDir;
131751
+ function probeSafariVersion(timeoutMs) {
131752
+ return new Promise((resolve) => {
131753
+ let child;
131754
+ try {
131755
+ child = (0, import_node_child_process3.spawn)("defaults", [
131756
+ "read",
131757
+ "/Applications/Safari.app/Contents/Info.plist",
131758
+ "CFBundleShortVersionString"
131759
+ ], { stdio: ["ignore", "pipe", "ignore"] });
131760
+ } catch {
131761
+ resolve(null);
131762
+ return;
131763
+ }
131764
+ let stdout = "";
131765
+ let settled = false;
131766
+ const finish = (value) => {
131767
+ if (settled)
131768
+ return;
131769
+ settled = true;
131770
+ clearTimeout(timer);
131771
+ try {
131772
+ child.kill();
131773
+ } catch {
131774
+ }
131775
+ resolve(value);
131776
+ };
131777
+ const timer = setTimeout(() => finish(null), timeoutMs);
131778
+ timer.unref?.();
131779
+ child.stdout?.on("data", (c) => {
131780
+ stdout += typeof c === "string" ? c : c.toString("utf8");
131781
+ });
131782
+ child.on("error", () => finish(null));
131783
+ child.on("close", (code) => finish(code === 0 ? stdout.trim() : null));
131784
+ });
131785
+ }
131786
+ async function getBrowserDiagnostics({ config }) {
131787
+ let detectionFailed = false;
131788
+ let record;
131789
+ try {
131790
+ record = readInstalledRecord({ cacheDir: config?.cacheDir });
131791
+ } catch {
131792
+ detectionFailed = true;
131793
+ record = { npmPackages: {}, browsers: {} };
131794
+ }
131795
+ const browsersRec = record.browsers || {};
131796
+ const platform = config?.environment?.platform;
131797
+ const brow = (name) => browsersRec[name];
131798
+ const npmInstalled = (name) => Boolean(resolveHeavyDepPath(name, { cacheDir: config?.cacheDir }));
131799
+ const browsers = [];
131800
+ const chrome = brow("chrome");
131801
+ const chromedriver = brow("chromedriver");
131802
+ const appiumChromium = npmInstalled("appium-chromium-driver");
131803
+ browsers.push({
131804
+ name: "chrome",
131805
+ supported: true,
131806
+ available: Boolean(chrome && chromedriver && appiumChromium),
131807
+ components: [
131808
+ { label: "chrome browser", installed: Boolean(chrome), detail: chrome?.installedVersion },
131809
+ { label: "chromedriver", installed: Boolean(chromedriver), detail: chromedriver?.installedVersion },
131810
+ { label: "appium-chromium-driver", installed: appiumChromium }
131811
+ ]
131812
+ });
131813
+ const firefox = brow("firefox");
131814
+ const geckodriver = brow("geckodriver");
131815
+ const appiumGecko = npmInstalled("appium-geckodriver");
131816
+ browsers.push({
131817
+ name: "firefox",
131818
+ supported: true,
131819
+ available: Boolean(firefox && geckodriver && appiumGecko),
131820
+ components: [
131821
+ { label: "firefox browser", installed: Boolean(firefox), detail: firefox?.installedVersion },
131822
+ { label: "geckodriver", installed: Boolean(geckodriver), detail: geckodriver?.installedVersion },
131823
+ { label: "appium-geckodriver", installed: appiumGecko }
131824
+ ]
131825
+ });
131826
+ const isMac = platform === "mac";
131827
+ const appiumSafari = npmInstalled("appium-safari-driver");
131828
+ let safariApp = false;
131829
+ let safariVersion;
131830
+ let safaridriver = false;
131831
+ if (isMac) {
131832
+ try {
131833
+ const version = await probeSafariVersion(4e3);
131834
+ safariApp = version !== null;
131835
+ safariVersion = version ?? void 0;
131836
+ safaridriver = import_node_fs7.default.existsSync("/usr/bin/safaridriver");
131837
+ } catch {
131838
+ detectionFailed = true;
131839
+ }
131840
+ }
131841
+ browsers.push({
131842
+ name: "safari",
131843
+ supported: isMac,
131844
+ available: Boolean(isMac && safariApp && safaridriver && appiumSafari),
131845
+ components: [
131846
+ { label: "Safari app", installed: safariApp, detail: safariVersion },
131847
+ {
131848
+ label: "safaridriver",
131849
+ installed: safaridriver,
131850
+ detail: safaridriver ? "/usr/bin/safaridriver" : void 0
131851
+ },
131852
+ { label: "appium-safari-driver", installed: appiumSafari }
131853
+ ],
131854
+ note: isMac ? void 0 : "Safari is only available on macOS"
131855
+ });
131856
+ return { browsers, detectionFailed };
131857
+ }
131858
+ var import_node_os3, import_node_fs7, import_node_path7, import_node_child_process3, import_node_url5, __dirname4, platformMap, defaultFileTypes, cachedAppsByDir;
131726
131859
  var init_config = __esm({
131727
131860
  "dist/core/config.js"() {
131728
131861
  "use strict";
131729
131862
  import_node_os3 = __toESM(require("node:os"), 1);
131863
+ import_node_fs7 = __toESM(require("node:fs"), 1);
131730
131864
  init_validate();
131731
131865
  init_utils();
131732
131866
  import_node_path7 = __toESM(require("node:path"), 1);
@@ -132020,8 +132154,19 @@ var browsers_exports = {};
132020
132154
  __export(browsers_exports, {
132021
132155
  BROWSER_CHANNELS: () => BROWSER_CHANNELS,
132022
132156
  ensureBrowserInstalled: () => ensureBrowserInstalled,
132023
- getInstalledBrowsers: () => getInstalledBrowsers
132157
+ getInstalledBrowsers: () => getInstalledBrowsers,
132158
+ requiredBrowserAssets: () => requiredBrowserAssets
132024
132159
  });
132160
+ function requiredBrowserAssets(name) {
132161
+ switch ((name ?? "").toLowerCase()) {
132162
+ case "chrome":
132163
+ return ["chrome", "chromedriver"];
132164
+ case "firefox":
132165
+ return ["firefox", "geckodriver"];
132166
+ default:
132167
+ return [];
132168
+ }
132169
+ }
132025
132170
  async function loadPuppeteerBrowsers(deps, ctx) {
132026
132171
  if (deps.browsersModule)
132027
132172
  return deps.browsersModule;
@@ -132399,7 +132544,7 @@ module.exports = __toCommonJS(index_exports);
132399
132544
  init_config();
132400
132545
 
132401
132546
  // dist/core/detectTests.js
132402
- var import_node_fs8 = __toESM(require("node:fs"), 1);
132547
+ var import_node_fs9 = __toESM(require("node:fs"), 1);
132403
132548
  var import_node_path9 = __toESM(require("node:path"), 1);
132404
132549
  var import_node_os5 = __toESM(require("node:os"), 1);
132405
132550
  var import_node_crypto3 = __toESM(require("node:crypto"), 1);
@@ -133231,7 +133376,7 @@ init_files();
133231
133376
  init_utils();
133232
133377
 
133233
133378
  // dist/core/integrations/heretto.js
133234
- var import_node_fs7 = __toESM(require("node:fs"), 1);
133379
+ var import_node_fs8 = __toESM(require("node:fs"), 1);
133235
133380
  var import_node_path8 = __toESM(require("node:path"), 1);
133236
133381
  var import_node_https = __toESM(require("node:https"), 1);
133237
133382
  var import_node_http = __toESM(require("node:http"), 1);
@@ -133392,11 +133537,11 @@ var HerettoUploader = class {
133392
133537
  return result;
133393
133538
  }
133394
133539
  }
133395
- if (!import_node_fs7.default.existsSync(localFilePath)) {
133540
+ if (!import_node_fs8.default.existsSync(localFilePath)) {
133396
133541
  result.description = `Local file not found: ${localFilePath}`;
133397
133542
  return result;
133398
133543
  }
133399
- const fileContent = import_node_fs7.default.readFileSync(localFilePath);
133544
+ const fileContent = import_node_fs8.default.readFileSync(localFilePath);
133400
133545
  const contentType = this.getContentType(localFilePath);
133401
133546
  try {
133402
133547
  await this.uploadFile({
@@ -134170,7 +134315,7 @@ async function pollJobStatus(client, fileId, jobId, log3, config) {
134170
134315
  return null;
134171
134316
  }
134172
134317
  async function downloadAndExtractOutput(client, fileId, jobId, herettoName, log3, config, deps) {
134173
- const fsModule = deps?.fsModule || import_node_fs7.default;
134318
+ const fsModule = deps?.fsModule || import_node_fs8.default;
134174
134319
  const ZipClass = deps?.ZipClass || import_adm_zip.default;
134175
134320
  const tempDir = import_node_path8.default.join(import_node_os4.default.tmpdir(), "doc-detective");
134176
134321
  const hash = import_node_crypto2.default.createHash("md5").update(`${herettoName}_${jobId}`).digest("hex");
@@ -134408,7 +134553,7 @@ async function isValidSourceFile({ config, files, source }) {
134408
134553
  } else {
134409
134554
  beforePath = import_node_path9.default.resolve(test.before);
134410
134555
  }
134411
- if (!import_node_fs8.default.existsSync(beforePath)) {
134556
+ if (!import_node_fs9.default.existsSync(beforePath)) {
134412
134557
  log(config, "debug", `${beforePath} is specified to run before a test but isn't a valid file. Skipping ${source}.`);
134413
134558
  return false;
134414
134559
  }
@@ -134420,7 +134565,7 @@ async function isValidSourceFile({ config, files, source }) {
134420
134565
  } else {
134421
134566
  afterPath = import_node_path9.default.resolve(test.after);
134422
134567
  }
134423
- if (!import_node_fs8.default.existsSync(afterPath)) {
134568
+ if (!import_node_fs9.default.existsSync(afterPath)) {
134424
134569
  log(config, "debug", `${afterPath} is specified to run after a test but isn't a valid file. Skipping ${source}.`);
134425
134570
  return false;
134426
134571
  }
@@ -134438,9 +134583,9 @@ async function processDitaMap({ config, source }) {
134438
134583
  const hash = import_node_crypto3.default.createHash("md5").update(source).digest("hex");
134439
134584
  const tmpBase = import_node_path9.default.join(import_node_os5.default.tmpdir(), "doc-detective");
134440
134585
  const outputDir = import_node_path9.default.join(tmpBase, `ditamap_${hash}`);
134441
- if (!import_node_fs8.default.existsSync(tmpBase)) {
134586
+ if (!import_node_fs9.default.existsSync(tmpBase)) {
134442
134587
  log(config, "debug", `Creating temp directory: ${tmpBase}`);
134443
- import_node_fs8.default.mkdirSync(tmpBase, { recursive: true });
134588
+ import_node_fs9.default.mkdirSync(tmpBase, { recursive: true });
134444
134589
  }
134445
134590
  const ditaVersion = await spawnCommand("dita", ["--version"]);
134446
134591
  if (ditaVersion.exitCode !== 0) {
@@ -134542,8 +134687,8 @@ async function qualifyFiles({ config }) {
134542
134687
  let isFile = false;
134543
134688
  let isDir = false;
134544
134689
  try {
134545
- isFile = import_node_fs8.default.statSync(source).isFile();
134546
- isDir = import_node_fs8.default.statSync(source).isDirectory();
134690
+ isFile = import_node_fs9.default.statSync(source).isFile();
134691
+ isDir = import_node_fs9.default.statSync(source).isDirectory();
134547
134692
  } catch {
134548
134693
  log(config, "warning", `Cannot access path: ${source}. Skipping.`);
134549
134694
  continue;
@@ -134563,13 +134708,13 @@ async function qualifyFiles({ config }) {
134563
134708
  dirs = [];
134564
134709
  dirs[0] = source;
134565
134710
  for (const dir of dirs) {
134566
- const objects = import_node_fs8.default.readdirSync(dir);
134711
+ const objects = import_node_fs9.default.readdirSync(dir);
134567
134712
  for (const object of objects) {
134568
134713
  const content = import_node_path9.default.resolve(dir + "/" + object);
134569
134714
  if (content.includes("node_modules"))
134570
134715
  continue;
134571
- const isFile2 = import_node_fs8.default.statSync(content).isFile();
134572
- const isDir2 = import_node_fs8.default.statSync(content).isDirectory();
134716
+ const isFile2 = import_node_fs9.default.statSync(content).isFile();
134717
+ const isDir2 = import_node_fs9.default.statSync(content).isDirectory();
134573
134718
  if (isFile2 && await isValidSourceFile({ config, files, source: content })) {
134574
134719
  files.push(import_node_path9.default.resolve(content));
134575
134720
  } else if (isDir2 && config.recursive) {
@@ -134590,7 +134735,7 @@ async function parseTests({ config, files }) {
134590
134735
  let rawContent;
134591
134736
  if (extension === "json" || extension === "yaml" || extension === "yml") {
134592
134737
  try {
134593
- rawContent = await import_node_fs8.default.promises.readFile(file, "utf8");
134738
+ rawContent = await import_node_fs9.default.promises.readFile(file, "utf8");
134594
134739
  if (extension === "json") {
134595
134740
  content = JSON.parse(rawContent);
134596
134741
  } else {
@@ -134976,6 +135121,7 @@ init_utils();
134976
135121
  // dist/core/tests.js
134977
135122
  var import_tree_kill = __toESM(require("tree-kill"), 1);
134978
135123
  init_loader();
135124
+ init_browsers();
134979
135125
  var import_node_os8 = __toESM(require("node:os"), 1);
134980
135126
  init_utils();
134981
135127
  var import_axios6 = __toESM(require("axios"), 1);
@@ -136236,7 +136382,7 @@ async function waitForDOMStable(driver, idleTime, timeout) {
136236
136382
  // dist/core/tests/runShell.js
136237
136383
  init_validate();
136238
136384
  init_utils();
136239
- var import_node_fs9 = __toESM(require("node:fs"), 1);
136385
+ var import_node_fs10 = __toESM(require("node:fs"), 1);
136240
136386
  var import_node_path10 = __toESM(require("node:path"), 1);
136241
136387
  async function runShell({ config, step }) {
136242
136388
  const result = {
@@ -136311,23 +136457,23 @@ async function runShell({ config, step }) {
136311
136457
  }
136312
136458
  if (step.runShell.path) {
136313
136459
  const dir = import_node_path10.default.dirname(step.runShell.path);
136314
- if (!import_node_fs9.default.existsSync(dir)) {
136315
- import_node_fs9.default.mkdirSync(dir, { recursive: true });
136460
+ if (!import_node_fs10.default.existsSync(dir)) {
136461
+ import_node_fs10.default.mkdirSync(dir, { recursive: true });
136316
136462
  }
136317
136463
  let filePath = step.runShell.path;
136318
136464
  log(config, "debug", `Saving stdio to file: ${filePath}`);
136319
- if (!import_node_fs9.default.existsSync(filePath)) {
136320
- import_node_fs9.default.writeFileSync(filePath, result.outputs.stdio.stdout);
136465
+ if (!import_node_fs10.default.existsSync(filePath)) {
136466
+ import_node_fs10.default.writeFileSync(filePath, result.outputs.stdio.stdout);
136321
136467
  } else {
136322
136468
  if (step.runShell.overwrite == "false") {
136323
136469
  result.description = result.description + ` Didn't save output. File already exists.`;
136324
136470
  }
136325
- const existingFile = import_node_fs9.default.readFileSync(filePath, "utf8");
136471
+ const existingFile = import_node_fs10.default.readFileSync(filePath, "utf8");
136326
136472
  const fractionalDiff = calculateFractionalDifference(existingFile, result.outputs.stdio.stdout);
136327
136473
  log(config, "debug", `Fractional difference: ${fractionalDiff}`);
136328
136474
  if (fractionalDiff > step.runShell.maxVariation) {
136329
136475
  if (step.runShell.overwrite == "aboveVariation") {
136330
- import_node_fs9.default.writeFileSync(filePath, result.outputs.stdio.stdout);
136476
+ import_node_fs10.default.writeFileSync(filePath, result.outputs.stdio.stdout);
136331
136477
  result.description += ` Saved output to file.`;
136332
136478
  }
136333
136479
  result.status = "WARNING";
@@ -136335,7 +136481,7 @@ async function runShell({ config, step }) {
136335
136481
  return result;
136336
136482
  }
136337
136483
  if (step.runShell.overwrite == "true") {
136338
- import_node_fs9.default.writeFileSync(filePath, result.outputs.stdio.stdout);
136484
+ import_node_fs10.default.writeFileSync(filePath, result.outputs.stdio.stdout);
136339
136485
  result.description += ` Saved output to file.`;
136340
136486
  }
136341
136487
  }
@@ -136520,7 +136666,7 @@ async function checkLink({ config, step }) {
136520
136666
  init_validate();
136521
136667
  init_utils();
136522
136668
  var import_node_path11 = __toESM(require("node:path"), 1);
136523
- var import_node_fs10 = __toESM(require("node:fs"), 1);
136669
+ var import_node_fs11 = __toESM(require("node:fs"), 1);
136524
136670
  init_loader();
136525
136671
  var _pngjs = null;
136526
136672
  var _sharp = null;
@@ -136647,8 +136793,8 @@ async function saveScreenshot({ config, step, driver }) {
136647
136793
  const rawBase = import_node_path11.default.basename(urlPathname.split("?")[0].split("#")[0].replace(/\\/g, "/"));
136648
136794
  const safeBase = sanitizeFilesystemName(rawBase, `${step.stepId}.png`);
136649
136795
  dir = import_node_path11.default.join(process.cwd(), "doc-detective-runs", getOrInitRunTimestamp(config));
136650
- if (!import_node_fs10.default.existsSync(dir)) {
136651
- import_node_fs10.default.mkdirSync(dir, { recursive: true });
136796
+ if (!import_node_fs11.default.existsSync(dir)) {
136797
+ import_node_fs11.default.mkdirSync(dir, { recursive: true });
136652
136798
  }
136653
136799
  const captureId = `${step.stepId || "screenshot"}_${Date.now()}`;
136654
136800
  filePath = import_node_path11.default.join(dir, `${captureId}_${safeBase}`);
@@ -136664,10 +136810,10 @@ async function saveScreenshot({ config, step, driver }) {
136664
136810
  }
136665
136811
  } else {
136666
136812
  dir = import_node_path11.default.dirname(step.screenshot.path);
136667
- if (!import_node_fs10.default.existsSync(dir)) {
136668
- import_node_fs10.default.mkdirSync(dir, { recursive: true });
136813
+ if (!import_node_fs11.default.existsSync(dir)) {
136814
+ import_node_fs11.default.mkdirSync(dir, { recursive: true });
136669
136815
  }
136670
- if (import_node_fs10.default.existsSync(filePath)) {
136816
+ if (import_node_fs11.default.existsSync(filePath)) {
136671
136817
  if (step.screenshot.overwrite == "false") {
136672
136818
  result.status = "SKIPPED";
136673
136819
  result.description = `File already exists: ${filePath}`;
@@ -136763,8 +136909,8 @@ async function saveScreenshot({ config, step, driver }) {
136763
136909
  } catch (error) {
136764
136910
  result.status = "FAIL";
136765
136911
  result.description = `Couldn't save screenshot. ${error}`;
136766
- if (existFilePath && filePath !== existFilePath && import_node_fs10.default.existsSync(filePath)) {
136767
- import_node_fs10.default.unlinkSync(filePath);
136912
+ if (existFilePath && filePath !== existFilePath && import_node_fs11.default.existsSync(filePath)) {
136913
+ import_node_fs11.default.unlinkSync(filePath);
136768
136914
  }
136769
136915
  return result;
136770
136916
  }
@@ -136816,12 +136962,12 @@ async function saveScreenshot({ config, step, driver }) {
136816
136962
  width: rect.width,
136817
136963
  height: rect.height
136818
136964
  }).toFile(croppedPath);
136819
- import_node_fs10.default.renameSync(croppedPath, filePath);
136965
+ import_node_fs11.default.renameSync(croppedPath, filePath);
136820
136966
  } catch (error) {
136821
136967
  result.status = "FAIL";
136822
136968
  result.description = `Couldn't crop image. ${error}`;
136823
- if (existFilePath && filePath !== existFilePath && import_node_fs10.default.existsSync(filePath)) {
136824
- import_node_fs10.default.unlinkSync(filePath);
136969
+ if (existFilePath && filePath !== existFilePath && import_node_fs11.default.existsSync(filePath)) {
136970
+ import_node_fs11.default.unlinkSync(filePath);
136825
136971
  }
136826
136972
  return result;
136827
136973
  }
@@ -136829,7 +136975,7 @@ async function saveScreenshot({ config, step, driver }) {
136829
136975
  if (existFilePath) {
136830
136976
  if (step.screenshot.overwrite == "true" && !isUrlPath) {
136831
136977
  result.description += ` Overwrote existing file.`;
136832
- import_node_fs10.default.renameSync(filePath, existFilePath);
136978
+ import_node_fs11.default.renameSync(filePath, existFilePath);
136833
136979
  result.outputs.screenshotPath = existFilePath;
136834
136980
  result.outputs.changed = true;
136835
136981
  if (step.screenshot.sourceIntegration) {
@@ -136842,21 +136988,21 @@ async function saveScreenshot({ config, step, driver }) {
136842
136988
  let img1;
136843
136989
  let img2;
136844
136990
  try {
136845
- img1 = PNG.sync.read(import_node_fs10.default.readFileSync(existFilePath));
136846
- img2 = PNG.sync.read(import_node_fs10.default.readFileSync(filePath));
136991
+ img1 = PNG.sync.read(import_node_fs11.default.readFileSync(existFilePath));
136992
+ img2 = PNG.sync.read(import_node_fs11.default.readFileSync(filePath));
136847
136993
  } catch (error) {
136848
136994
  result.status = "FAIL";
136849
136995
  result.description = isUrlPath ? `Couldn't decode PNG for comparison. The URL reference (${redactedUrl}) may not be a valid PNG. ${error}` : `Couldn't decode PNG for comparison. ${error}`;
136850
- if (!isUrlPath && filePath !== existFilePath && import_node_fs10.default.existsSync(filePath)) {
136851
- import_node_fs10.default.unlinkSync(filePath);
136996
+ if (!isUrlPath && filePath !== existFilePath && import_node_fs11.default.existsSync(filePath)) {
136997
+ import_node_fs11.default.unlinkSync(filePath);
136852
136998
  }
136853
136999
  return result;
136854
137000
  }
136855
137001
  if (!aspectRatiosMatch(img1, img2)) {
136856
137002
  result.status = "FAIL";
136857
137003
  result.description = `Couldn't compare images. Images have different aspect ratios.`;
136858
- if (!isUrlPath && filePath !== existFilePath && import_node_fs10.default.existsSync(filePath)) {
136859
- import_node_fs10.default.unlinkSync(filePath);
137004
+ if (!isUrlPath && filePath !== existFilePath && import_node_fs11.default.existsSync(filePath)) {
137005
+ import_node_fs11.default.unlinkSync(filePath);
136860
137006
  }
136861
137007
  return result;
136862
137008
  }
@@ -136883,8 +137029,8 @@ async function saveScreenshot({ config, step, driver }) {
136883
137029
  } catch (error) {
136884
137030
  result.status = "FAIL";
136885
137031
  result.description = `Couldn't load screenshot comparison dependency (pixelmatch). ${error?.message ?? error}`;
136886
- if (!isUrlPath && filePath !== existFilePath && import_node_fs10.default.existsSync(filePath)) {
136887
- import_node_fs10.default.unlinkSync(filePath);
137032
+ if (!isUrlPath && filePath !== existFilePath && import_node_fs11.default.existsSync(filePath)) {
137033
+ import_node_fs11.default.unlinkSync(filePath);
136888
137034
  }
136889
137035
  return result;
136890
137036
  }
@@ -136897,7 +137043,7 @@ async function saveScreenshot({ config, step, driver }) {
136897
137043
  });
136898
137044
  if (fractionalDiff > step.screenshot.maxVariation) {
136899
137045
  if (step.screenshot.overwrite == "aboveVariation" && !isUrlPath) {
136900
- import_node_fs10.default.renameSync(filePath, existFilePath);
137046
+ import_node_fs11.default.renameSync(filePath, existFilePath);
136901
137047
  }
136902
137048
  result.status = "WARNING";
136903
137049
  result.description += ` The difference between the existing screenshot and new screenshot (${fractionalDiff.toFixed(2)}) is greater than the max accepted variation (${step.screenshot.maxVariation}).`;
@@ -136923,7 +137069,7 @@ async function saveScreenshot({ config, step, driver }) {
136923
137069
  result.outputs.sourceIntegration = step.screenshot.sourceIntegration;
136924
137070
  }
136925
137071
  if (step.screenshot.overwrite != "true") {
136926
- import_node_fs10.default.unlinkSync(filePath);
137072
+ import_node_fs11.default.unlinkSync(filePath);
136927
137073
  }
136928
137074
  }
136929
137075
  }
@@ -136943,7 +137089,7 @@ async function saveScreenshot({ config, step, driver }) {
136943
137089
  init_validate();
136944
137090
  init_utils();
136945
137091
  var import_node_path12 = __toESM(require("node:path"), 1);
136946
- var import_node_fs11 = __toESM(require("node:fs"), 1);
137092
+ var import_node_fs12 = __toESM(require("node:fs"), 1);
136947
137093
  var import_node_os6 = __toESM(require("node:os"), 1);
136948
137094
  async function startRecording({ config, context, step, driver }) {
136949
137095
  let result = {
@@ -136981,10 +137127,10 @@ async function startRecording({ config, context, step, driver }) {
136981
137127
  let filePath = step.record.path;
136982
137128
  const baseName = import_node_path12.default.basename(filePath, import_node_path12.default.extname(filePath));
136983
137129
  const dir = import_node_path12.default.dirname(step.record.path);
136984
- if (!import_node_fs11.default.existsSync(dir)) {
136985
- import_node_fs11.default.mkdirSync(dir, { recursive: true });
137130
+ if (!import_node_fs12.default.existsSync(dir)) {
137131
+ import_node_fs12.default.mkdirSync(dir, { recursive: true });
136986
137132
  }
136987
- if (import_node_fs11.default.existsSync(filePath) && step.record.overwrite == "false") {
137133
+ if (import_node_fs12.default.existsSync(filePath) && step.record.overwrite == "false") {
136988
137134
  result.status = "SKIPPED";
136989
137135
  result.description = `File already exists: ${filePath}`;
136990
137136
  return result;
@@ -137108,7 +137254,7 @@ init_validate();
137108
137254
  init_utils();
137109
137255
  var import_node_child_process4 = require("node:child_process");
137110
137256
  var import_node_path13 = __toESM(require("node:path"), 1);
137111
- var import_node_fs12 = __toESM(require("node:fs"), 1);
137257
+ var import_node_fs13 = __toESM(require("node:fs"), 1);
137112
137258
  init_loader();
137113
137259
  async function getFfmpegPath(ctx = {}) {
137114
137260
  const mod = await loadHeavyDep("@ffmpeg-installer/ffmpeg", { ctx });
@@ -137157,11 +137303,11 @@ async function stopRecording({ config, step, driver }) {
137157
137303
  window.recorder.stop();
137158
137304
  });
137159
137305
  let waitCount = 0;
137160
- while (!import_node_fs12.default.existsSync(config.recording.downloadPath) && waitCount < 60) {
137306
+ while (!import_node_fs13.default.existsSync(config.recording.downloadPath) && waitCount < 60) {
137161
137307
  await new Promise((r) => setTimeout(r, 1e3));
137162
137308
  waitCount++;
137163
137309
  }
137164
- if (!import_node_fs12.default.existsSync(config.recording.downloadPath)) {
137310
+ if (!import_node_fs13.default.existsSync(config.recording.downloadPath)) {
137165
137311
  result.status = "FAIL";
137166
137312
  result.description = "Recording download timed out.";
137167
137313
  return result;
@@ -137186,7 +137332,7 @@ async function stopRecording({ config, step, driver }) {
137186
137332
  if (code === 0) {
137187
137333
  if (targetPath !== downloadPath) {
137188
137334
  try {
137189
- import_node_fs12.default.unlinkSync(downloadPath);
137335
+ import_node_fs13.default.unlinkSync(downloadPath);
137190
137336
  } catch {
137191
137337
  }
137192
137338
  }
@@ -137232,7 +137378,7 @@ async function loadVariables({ step }) {
137232
137378
  init_validate();
137233
137379
  init_utils();
137234
137380
  var import_node_path14 = __toESM(require("node:path"), 1);
137235
- var import_node_fs13 = __toESM(require("node:fs"), 1);
137381
+ var import_node_fs14 = __toESM(require("node:fs"), 1);
137236
137382
  async function saveCookie({ config, step, driver }) {
137237
137383
  let result = {
137238
137384
  status: "PASS",
@@ -137299,14 +137445,14 @@ async function saveCookie({ config, step, driver }) {
137299
137445
  if (filePath) {
137300
137446
  const outputDirectory = directory || config.output || process.cwd();
137301
137447
  const fullPath = import_node_path14.default.resolve(outputDirectory, filePath);
137302
- if (import_node_fs13.default.existsSync(fullPath) && !(overwrite === true || overwrite === "true")) {
137448
+ if (import_node_fs14.default.existsSync(fullPath) && !(overwrite === true || overwrite === "true")) {
137303
137449
  result.status = "FAIL";
137304
137450
  result.description = `File '${fullPath}' already exists and overwrite is not enabled.`;
137305
137451
  return result;
137306
137452
  }
137307
137453
  const dir = import_node_path14.default.dirname(fullPath);
137308
- if (!import_node_fs13.default.existsSync(dir)) {
137309
- import_node_fs13.default.mkdirSync(dir, { recursive: true });
137454
+ if (!import_node_fs14.default.existsSync(dir)) {
137455
+ import_node_fs14.default.mkdirSync(dir, { recursive: true });
137310
137456
  }
137311
137457
  if (targetCookie) {
137312
137458
  const netscapeCookie = formatCookieForNetscape(targetCookie);
@@ -137314,11 +137460,11 @@ async function saveCookie({ config, step, driver }) {
137314
137460
  # This is a cookie file saved by Doc Detective
137315
137461
  ${netscapeCookie}
137316
137462
  `;
137317
- import_node_fs13.default.writeFileSync(fullPath, content, "utf8");
137463
+ import_node_fs14.default.writeFileSync(fullPath, content, "utf8");
137318
137464
  result.description = `Saved cookie '${cookieName}' to '${fullPath}'.`;
137319
137465
  log(config, "debug", `Saved cookie '${cookieName}' to file: ${fullPath}`);
137320
137466
  } else {
137321
- import_node_fs13.default.writeFileSync(fullPath, "# No cookie data\n", "utf8");
137467
+ import_node_fs14.default.writeFileSync(fullPath, "# No cookie data\n", "utf8");
137322
137468
  result.description = `Created empty cookie file at '${fullPath}'.`;
137323
137469
  log(config, "debug", `Created empty cookie file: ${fullPath}`);
137324
137470
  }
@@ -137346,7 +137492,7 @@ function formatCookieForNetscape(cookie) {
137346
137492
  init_validate();
137347
137493
  init_utils();
137348
137494
  var import_node_path15 = __toESM(require("node:path"), 1);
137349
- var import_node_fs14 = __toESM(require("node:fs"), 1);
137495
+ var import_node_fs15 = __toESM(require("node:fs"), 1);
137350
137496
  async function loadCookie({ config, step, driver }) {
137351
137497
  let result = {
137352
137498
  status: "PASS",
@@ -137402,13 +137548,13 @@ async function loadCookie({ config, step, driver }) {
137402
137548
  } else if (filePath) {
137403
137549
  const inputDirectory = directory || config.output || process.cwd();
137404
137550
  const fullPath = import_node_path15.default.resolve(inputDirectory, filePath);
137405
- if (!import_node_fs14.default.existsSync(fullPath)) {
137551
+ if (!import_node_fs15.default.existsSync(fullPath)) {
137406
137552
  result.status = "FAIL";
137407
137553
  result.description = `Cookie file '${fullPath}' not found`;
137408
137554
  return result;
137409
137555
  }
137410
137556
  try {
137411
- const fileContent = import_node_fs14.default.readFileSync(fullPath, "utf8");
137557
+ const fileContent = import_node_fs15.default.readFileSync(fullPath, "utf8");
137412
137558
  const cookies = parseNetscapeCookieFile(fileContent);
137413
137559
  if (cookies.length === 0) {
137414
137560
  result.status = "FAIL";
@@ -137574,7 +137720,7 @@ function isDomainCompatible(currentDomain, cookieDomain) {
137574
137720
  // dist/core/tests/httpRequest.js
137575
137721
  init_validate();
137576
137722
  var import_axios5 = __toESM(require("axios"), 1);
137577
- var import_node_fs15 = __toESM(require("node:fs"), 1);
137723
+ var import_node_fs16 = __toESM(require("node:fs"), 1);
137578
137724
  var import_node_path16 = __toESM(require("node:path"), 1);
137579
137725
  var import_ajv2 = __toESM(require("ajv"), 1);
137580
137726
  init_openapi();
@@ -137881,24 +138027,24 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
137881
138027
  }
137882
138028
  if (step.httpRequest.path) {
137883
138029
  const dir = import_node_path16.default.dirname(step.httpRequest.path);
137884
- if (!import_node_fs15.default.existsSync(dir)) {
137885
- import_node_fs15.default.mkdirSync(dir, { recursive: true });
138030
+ if (!import_node_fs16.default.existsSync(dir)) {
138031
+ import_node_fs16.default.mkdirSync(dir, { recursive: true });
137886
138032
  }
137887
138033
  let filePath = step.httpRequest.path;
137888
138034
  log(config, "debug", `Saving output to file: ${filePath}`);
137889
- if (!import_node_fs15.default.existsSync(filePath)) {
137890
- await import_node_fs15.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
138035
+ if (!import_node_fs16.default.existsSync(filePath)) {
138036
+ await import_node_fs16.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
137891
138037
  result.description += ` Saved output to file.`;
137892
138038
  } else {
137893
138039
  if (step.httpRequest.overwrite == "false") {
137894
138040
  result.description += ` Didn't save output. File already exists.`;
137895
138041
  }
137896
- const existingFile = import_node_fs15.default.readFileSync(filePath, "utf8");
138042
+ const existingFile = import_node_fs16.default.readFileSync(filePath, "utf8");
137897
138043
  const fractionalDiff = calculateFractionalDifference(existingFile, JSON.stringify(response.data, null, 2));
137898
138044
  log(config, "debug", `Fractional difference: ${fractionalDiff}`);
137899
138045
  if (fractionalDiff > step.httpRequest.maxVariation) {
137900
138046
  if (step.httpRequest.overwrite == "aboveVariation") {
137901
- await import_node_fs15.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
138047
+ await import_node_fs16.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
137902
138048
  result.description += ` Saved response to file.`;
137903
138049
  }
137904
138050
  result.status = "WARNING";
@@ -137906,7 +138052,7 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
137906
138052
  return result;
137907
138053
  }
137908
138054
  if (step.httpRequest.overwrite == "true") {
137909
- import_node_fs15.default.writeFileSync(filePath, JSON.stringify(response.data, null, 2));
138055
+ import_node_fs16.default.writeFileSync(filePath, JSON.stringify(response.data, null, 2));
137910
138056
  result.description += ` Saved response to file.`;
137911
138057
  }
137912
138058
  }
@@ -138083,7 +138229,7 @@ async function clickElement({ config, step, driver }) {
138083
138229
  // dist/core/tests/runCode.js
138084
138230
  init_validate();
138085
138231
  init_utils();
138086
- var import_node_fs16 = __toESM(require("node:fs"), 1);
138232
+ var import_node_fs17 = __toESM(require("node:fs"), 1);
138087
138233
  var import_node_path17 = __toESM(require("node:path"), 1);
138088
138234
  var import_node_os7 = __toESM(require("node:os"), 1);
138089
138235
  function createTempScript(code, language) {
@@ -138107,7 +138253,7 @@ function createTempScript(code, language) {
138107
138253
  const tmpDir = import_node_os7.default.tmpdir();
138108
138254
  const tmpFile = import_node_path17.default.join(tmpDir, `doc-detective-${Date.now()}${extension}`);
138109
138255
  try {
138110
- import_node_fs16.default.writeFileSync(tmpFile, code);
138256
+ import_node_fs17.default.writeFileSync(tmpFile, code);
138111
138257
  } catch (error) {
138112
138258
  throw new Error(`Failed to create temporary script: ${error.message}`);
138113
138259
  }
@@ -138191,7 +138337,7 @@ async function runCode({ config, step }) {
138191
138337
  result.description = error.message;
138192
138338
  } finally {
138193
138339
  try {
138194
- import_node_fs16.default.unlinkSync(scriptPath);
138340
+ import_node_fs17.default.unlinkSync(scriptPath);
138195
138341
  log(config, "debug", `Removed temporary script: ${scriptPath}`);
138196
138342
  } catch (error) {
138197
138343
  log(config, "warn", `Failed to remove temporary script: ${scriptPath}`);
@@ -138739,6 +138885,14 @@ var driverActions2 = [
138739
138885
  "type"
138740
138886
  ];
138741
138887
  var KNOWN_BROWSERS = ["firefox", "chrome", "safari", "webkit"];
138888
+ function combinationKey(context) {
138889
+ const rawName = context?.browser?.name;
138890
+ const name = rawName === "webkit" ? "safari" : rawName || "<none>";
138891
+ return `${context?.platform}::${name}`;
138892
+ }
138893
+ function warmUpDecision(prev) {
138894
+ return prev === "failed" ? "skip" : "attempt";
138895
+ }
138742
138896
  function getDriverCapabilities({ runnerDetails, name, options }) {
138743
138897
  let capabilities = {};
138744
138898
  let args = [];
@@ -139017,8 +139171,10 @@ async function runSpecs({ resolvedTests }) {
139017
139171
  allowUnsafeSteps: await allowUnsafeSteps({ config })
139018
139172
  };
139019
139173
  const platform = runnerDetails.environment.platform;
139020
- const availableApps = runnerDetails.availableApps;
139174
+ let availableApps = runnerDetails.availableApps;
139021
139175
  const metaValues = { specs: {} };
139176
+ const installAttempts = /* @__PURE__ */ new Map();
139177
+ const warmUpResults = /* @__PURE__ */ new Map();
139022
139178
  let appium;
139023
139179
  const report = {
139024
139180
  summary: {
@@ -139133,14 +139289,46 @@ async function runSpecs({ resolvedTests }) {
139133
139289
  testReport.contexts.push(contextReport);
139134
139290
  continue;
139135
139291
  }
139136
- const supportedContext = isSupportedContext({
139292
+ let supportedContext = isSupportedContext({
139137
139293
  context,
139138
139294
  apps: availableApps,
139139
139295
  platform
139140
139296
  });
139297
+ let freshInstallRedetected = false;
139298
+ if (!supportedContext && context.platform === platform && // Mirror isSupportedContext's own guard: isDriverRequired iterates
139299
+ // context.steps, so a malformed context without a steps array would
139300
+ // otherwise crash the loop here instead of skipping cleanly.
139301
+ Array.isArray(context?.steps) && isDriverRequired({ test: context }) && requiredBrowserAssets(context.browser?.name).length > 0) {
139302
+ const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
139303
+ const outcome = await ensureContextBrowserInstalled({
139304
+ browserName: context.browser?.name,
139305
+ config,
139306
+ installAttempts,
139307
+ deps: {
139308
+ ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
139309
+ log
139310
+ }
139311
+ });
139312
+ if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
139313
+ freshInstallRedetected = true;
139314
+ clearAppCache(config);
139315
+ availableApps = await getAvailableApps({ config });
139316
+ runnerDetails.availableApps = availableApps;
139317
+ supportedContext = isSupportedContext({
139318
+ context,
139319
+ apps: availableApps,
139320
+ platform
139321
+ });
139322
+ }
139323
+ }
139141
139324
  if (!supportedContext) {
139142
- log(config, "info", `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`);
139143
- contextReport = { result: "SKIPPED", ...contextReport };
139325
+ const errorMessage = freshInstallRedetected ? `Skipping context '${context.browser?.name}' on '${context.platform}': the missing browser dependency was installed but still could not be detected.` : `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`;
139326
+ log(config, freshInstallRedetected ? "warning" : "info", errorMessage);
139327
+ contextReport = {
139328
+ ...contextReport,
139329
+ result: "SKIPPED",
139330
+ resultDescription: errorMessage
139331
+ };
139144
139332
  report.summary.contexts.skipped++;
139145
139333
  testReport.contexts.push(contextReport);
139146
139334
  continue;
@@ -139153,6 +139341,19 @@ ${JSON.stringify(context, null, 2)}`);
139153
139341
  }
139154
139342
  const driverRequired = isDriverRequired({ test: context });
139155
139343
  if (driverRequired) {
139344
+ const combo = combinationKey(context);
139345
+ if (warmUpDecision(warmUpResults.get(combo)) === "skip") {
139346
+ const errorMessage = `Skipping context '${context.browser?.name}' on '${context.platform}': this context combination could not start a driver earlier in this run.`;
139347
+ log(config, "warning", errorMessage);
139348
+ contextReport = {
139349
+ ...contextReport,
139350
+ result: "SKIPPED",
139351
+ resultDescription: errorMessage
139352
+ };
139353
+ report.summary.contexts.skipped++;
139354
+ testReport.contexts.push(contextReport);
139355
+ continue;
139356
+ }
139156
139357
  let caps = getDriverCapabilities({
139157
139358
  runnerDetails,
139158
139359
  name: context.browser.name,
@@ -139185,19 +139386,23 @@ ${JSON.stringify(context, null, 2)}`);
139185
139386
  driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
139186
139387
  } catch (error2) {
139187
139388
  let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
139188
- if (context.browser?.name === "safari")
139389
+ if (context.browser?.name === "safari" || context.browser?.name === "webkit")
139189
139390
  errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
139190
139391
  log(config, "error", errorMessage);
139392
+ if (!warmUpResults.has(combo))
139393
+ warmUpResults.set(combo, "failed");
139191
139394
  contextReport = {
139395
+ ...contextReport,
139192
139396
  result: "SKIPPED",
139193
- resultDescription: errorMessage,
139194
- ...contextReport
139397
+ resultDescription: errorMessage
139195
139398
  };
139196
139399
  report.summary.contexts.skipped++;
139197
139400
  testReport.contexts.push(contextReport);
139198
139401
  continue;
139199
139402
  }
139200
139403
  }
139404
+ if (!warmUpResults.has(combo))
139405
+ warmUpResults.set(combo, "ok");
139201
139406
  if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
139202
139407
  await setViewportSize(context, driver);
139203
139408
  } else if (context.browser?.window?.width || context.browser?.window?.height) {
@@ -139533,6 +139738,31 @@ async function ensureChromeAvailable(config, deps) {
139533
139738
  }
139534
139739
  return availableApps;
139535
139740
  }
139741
+ async function ensureContextBrowserInstalled({ browserName, config, installAttempts, deps }) {
139742
+ const key = (browserName ?? "<none>").toLowerCase();
139743
+ const cached = installAttempts.get(key);
139744
+ if (cached)
139745
+ return cached;
139746
+ const assets = requiredBrowserAssets(browserName);
139747
+ if (assets.length === 0) {
139748
+ installAttempts.set(key, "notInstallable");
139749
+ return "notInstallable";
139750
+ }
139751
+ const ctx = { cacheDir: config?.cacheDir };
139752
+ const logger = (msg, level = "info") => deps.log?.(config, level === "warn" ? "warning" : level, msg);
139753
+ try {
139754
+ deps.log?.(config, "info", `Browser '${browserName}' is not available; attempting on-demand install of: ${assets.join(", ")}.`);
139755
+ for (const asset of assets) {
139756
+ await deps.ensureBrowser(asset, { ctx, deps: { logger } });
139757
+ }
139758
+ installAttempts.set(key, "installed");
139759
+ return "installed";
139760
+ } catch (err) {
139761
+ deps.log?.(config, "warning", `On-demand install for '${browserName}' failed: ${err?.message ?? err}`);
139762
+ installAttempts.set(key, "failed");
139763
+ return "failed";
139764
+ }
139765
+ }
139536
139766
  async function getRunner(options = {}) {
139537
139767
  const environment = getEnvironment();
139538
139768
  const config = { ...options.config, environment };
@@ -139746,22 +139976,19 @@ async function runTests(config, options = {}) {
139746
139976
  if (needs.browsers.size > 0) {
139747
139977
  try {
139748
139978
  const { getAvailableApps: getAvailableApps2, clearAppCache: clearAppCache2 } = await Promise.resolve().then(() => (init_config(), config_exports));
139749
- const { ensureBrowserInstalled: ensureBrowserInstalled2 } = await Promise.resolve().then(() => (init_browsers(), browsers_exports));
139979
+ const { ensureBrowserInstalled: ensureBrowserInstalled2, requiredBrowserAssets: requiredBrowserAssets2 } = await Promise.resolve().then(() => (init_browsers(), browsers_exports));
139750
139980
  const available = await getAvailableApps2({ config });
139751
139981
  const availableNames = new Set(available.map((a) => a.name));
139752
139982
  let installedAnything = false;
139753
139983
  for (const browser of needs.browsers) {
139754
139984
  if (availableNames.has(browser))
139755
139985
  continue;
139756
- if (browser === "chrome") {
139757
- await ensureBrowserInstalled2("chrome", { ctx, deps: { logger: preflightLogger } });
139758
- await ensureBrowserInstalled2("chromedriver", { ctx, deps: { logger: preflightLogger } });
139759
- installedAnything = true;
139760
- } else if (browser === "firefox") {
139761
- await ensureBrowserInstalled2("firefox", { ctx, deps: { logger: preflightLogger } });
139762
- await ensureBrowserInstalled2("geckodriver", { ctx, deps: { logger: preflightLogger } });
139763
- installedAnything = true;
139986
+ const assets = requiredBrowserAssets2(browser);
139987
+ for (const asset of assets) {
139988
+ await ensureBrowserInstalled2(asset, { ctx, deps: { logger: preflightLogger } });
139764
139989
  }
139990
+ if (assets.length > 0)
139991
+ installedAnything = true;
139765
139992
  }
139766
139993
  if (installedAnything)
139767
139994
  clearAppCache2(config);