doc-detective 4.15.3 → 4.16.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.
Files changed (33) hide show
  1. package/dist/common/src/schemas/schemas.json +280 -0
  2. package/dist/common/src/types/generated/config_v3.d.ts +16 -0
  3. package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
  4. package/dist/common/src/types/generated/context_v3.d.ts +12 -0
  5. package/dist/common/src/types/generated/context_v3.d.ts.map +1 -1
  6. package/dist/common/src/types/generated/report_v3.d.ts +12 -0
  7. package/dist/common/src/types/generated/report_v3.d.ts.map +1 -1
  8. package/dist/common/src/types/generated/resolvedTests_v3.d.ts +28 -0
  9. package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
  10. package/dist/common/src/types/generated/spec_v3.d.ts +12 -0
  11. package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -1
  12. package/dist/common/src/types/generated/test_v3.d.ts +16 -0
  13. package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
  14. package/dist/core/config.d.ts +28 -1
  15. package/dist/core/config.d.ts.map +1 -1
  16. package/dist/core/config.js +90 -13
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/resolveTests.d.ts.map +1 -1
  19. package/dist/core/resolveTests.js +5 -0
  20. package/dist/core/resolveTests.js.map +1 -1
  21. package/dist/core/tests.d.ts +65 -2
  22. package/dist/core/tests.d.ts.map +1 -1
  23. package/dist/core/tests.js +312 -77
  24. package/dist/core/tests.js.map +1 -1
  25. package/dist/index.cjs +1190 -569
  26. package/dist/runtime/browsers.d.ts +39 -0
  27. package/dist/runtime/browsers.d.ts.map +1 -1
  28. package/dist/runtime/browsers.js +269 -39
  29. package/dist/runtime/browsers.js.map +1 -1
  30. package/dist/utils.d.ts.map +1 -1
  31. package/dist/utils.js +18 -0
  32. package/dist/utils.js.map +1 -1
  33. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -1037,6 +1037,11 @@ var init_schemas = __esm({
1037
1037
  ],
1038
1038
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
1039
1039
  },
1040
+ explicit: {
1041
+ type: "boolean",
1042
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
1043
+ readOnly: true
1044
+ },
1040
1045
  headless: {
1041
1046
  type: "boolean",
1042
1047
  description: "If `true`, runs the browser in headless mode.",
@@ -1111,6 +1116,11 @@ var init_schemas = __esm({
1111
1116
  ],
1112
1117
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
1113
1118
  },
1119
+ explicit: {
1120
+ type: "boolean",
1121
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
1122
+ readOnly: true
1123
+ },
1114
1124
  headless: {
1115
1125
  type: "boolean",
1116
1126
  description: "If `true`, runs the browser in headless mode.",
@@ -1155,6 +1165,15 @@ var init_schemas = __esm({
1155
1165
  }
1156
1166
  }
1157
1167
  ]
1168
+ },
1169
+ browserFallback: {
1170
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
1171
+ type: "string",
1172
+ enum: [
1173
+ "auto",
1174
+ "explicit",
1175
+ "off"
1176
+ ]
1158
1177
  }
1159
1178
  },
1160
1179
  components: {
@@ -1197,6 +1216,11 @@ var init_schemas = __esm({
1197
1216
  ],
1198
1217
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
1199
1218
  },
1219
+ explicit: {
1220
+ type: "boolean",
1221
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
1222
+ readOnly: true
1223
+ },
1200
1224
  headless: {
1201
1225
  type: "boolean",
1202
1226
  description: "If `true`, runs the browser in headless mode.",
@@ -29464,6 +29488,16 @@ var init_schemas = __esm({
29464
29488
  type: "string",
29465
29489
  minLength: 1,
29466
29490
  pattern: "\\S"
29491
+ },
29492
+ browserFallback: {
29493
+ description: "Controls whether a context whose browser cannot start a driver session falls back to another available browser instead of being skipped. Drivers are validated by execution (not just presence) so a present-but-broken driver \u2014 for example a partially downloaded geckodriver \u2014 no longer silently skips Firefox coverage. `auto` (default): fall back to any other available browser for both auto-selected and explicitly requested browsers; a fallback away from an explicitly requested browser reports the context as `WARNING` rather than `PASS`. `explicit`: fall back only when the browser was auto-selected; an explicitly requested browser whose driver is broken is skipped with a diagnostic reason. `off`: never fall back across browsers (driver validation and diagnostic skip reasons still apply). Equivalent to `--browser-fallback` on the CLI.",
29494
+ type: "string",
29495
+ enum: [
29496
+ "auto",
29497
+ "explicit",
29498
+ "off"
29499
+ ],
29500
+ default: "auto"
29467
29501
  }
29468
29502
  },
29469
29503
  components: {
@@ -56977,6 +57011,11 @@ var init_schemas = __esm({
56977
57011
  ],
56978
57012
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
56979
57013
  },
57014
+ explicit: {
57015
+ type: "boolean",
57016
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
57017
+ readOnly: true
57018
+ },
56980
57019
  headless: {
56981
57020
  type: "boolean",
56982
57021
  description: "If `true`, runs the browser in headless mode.",
@@ -57051,6 +57090,11 @@ var init_schemas = __esm({
57051
57090
  ],
57052
57091
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
57053
57092
  },
57093
+ explicit: {
57094
+ type: "boolean",
57095
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
57096
+ readOnly: true
57097
+ },
57054
57098
  headless: {
57055
57099
  type: "boolean",
57056
57100
  description: "If `true`, runs the browser in headless mode.",
@@ -57095,6 +57139,15 @@ var init_schemas = __esm({
57095
57139
  }
57096
57140
  }
57097
57141
  ]
57142
+ },
57143
+ browserFallback: {
57144
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
57145
+ type: "string",
57146
+ enum: [
57147
+ "auto",
57148
+ "explicit",
57149
+ "off"
57150
+ ]
57098
57151
  }
57099
57152
  },
57100
57153
  components: {
@@ -57137,6 +57190,11 @@ var init_schemas = __esm({
57137
57190
  ],
57138
57191
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
57139
57192
  },
57193
+ explicit: {
57194
+ type: "boolean",
57195
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
57196
+ readOnly: true
57197
+ },
57140
57198
  headless: {
57141
57199
  type: "boolean",
57142
57200
  description: "If `true`, runs the browser in headless mode.",
@@ -62241,6 +62299,11 @@ var init_schemas = __esm({
62241
62299
  ],
62242
62300
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
62243
62301
  },
62302
+ explicit: {
62303
+ type: "boolean",
62304
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
62305
+ readOnly: true
62306
+ },
62244
62307
  headless: {
62245
62308
  type: "boolean",
62246
62309
  description: "If `true`, runs the browser in headless mode.",
@@ -62315,6 +62378,11 @@ var init_schemas = __esm({
62315
62378
  ],
62316
62379
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
62317
62380
  },
62381
+ explicit: {
62382
+ type: "boolean",
62383
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
62384
+ readOnly: true
62385
+ },
62318
62386
  headless: {
62319
62387
  type: "boolean",
62320
62388
  description: "If `true`, runs the browser in headless mode.",
@@ -62359,6 +62427,15 @@ var init_schemas = __esm({
62359
62427
  }
62360
62428
  }
62361
62429
  ]
62430
+ },
62431
+ browserFallback: {
62432
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
62433
+ type: "string",
62434
+ enum: [
62435
+ "auto",
62436
+ "explicit",
62437
+ "off"
62438
+ ]
62362
62439
  }
62363
62440
  },
62364
62441
  components: {
@@ -62401,6 +62478,11 @@ var init_schemas = __esm({
62401
62478
  ],
62402
62479
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
62403
62480
  },
62481
+ explicit: {
62482
+ type: "boolean",
62483
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
62484
+ readOnly: true
62485
+ },
62404
62486
  headless: {
62405
62487
  type: "boolean",
62406
62488
  description: "If `true`, runs the browser in headless mode.",
@@ -90668,6 +90750,16 @@ var init_schemas = __esm({
90668
90750
  type: "string",
90669
90751
  minLength: 1,
90670
90752
  pattern: "\\S"
90753
+ },
90754
+ browserFallback: {
90755
+ description: "Controls whether a context whose browser cannot start a driver session falls back to another available browser instead of being skipped. Drivers are validated by execution (not just presence) so a present-but-broken driver \u2014 for example a partially downloaded geckodriver \u2014 no longer silently skips Firefox coverage. `auto` (default): fall back to any other available browser for both auto-selected and explicitly requested browsers; a fallback away from an explicitly requested browser reports the context as `WARNING` rather than `PASS`. `explicit`: fall back only when the browser was auto-selected; an explicitly requested browser whose driver is broken is skipped with a diagnostic reason. `off`: never fall back across browsers (driver validation and diagnostic skip reasons still apply). Equivalent to `--browser-fallback` on the CLI.",
90756
+ type: "string",
90757
+ enum: [
90758
+ "auto",
90759
+ "explicit",
90760
+ "off"
90761
+ ],
90762
+ default: "auto"
90671
90763
  }
90672
90764
  },
90673
90765
  components: {
@@ -118243,6 +118335,11 @@ var init_schemas = __esm({
118243
118335
  ],
118244
118336
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
118245
118337
  },
118338
+ explicit: {
118339
+ type: "boolean",
118340
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
118341
+ readOnly: true
118342
+ },
118246
118343
  headless: {
118247
118344
  type: "boolean",
118248
118345
  description: "If `true`, runs the browser in headless mode.",
@@ -118317,6 +118414,11 @@ var init_schemas = __esm({
118317
118414
  ],
118318
118415
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
118319
118416
  },
118417
+ explicit: {
118418
+ type: "boolean",
118419
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
118420
+ readOnly: true
118421
+ },
118320
118422
  headless: {
118321
118423
  type: "boolean",
118322
118424
  description: "If `true`, runs the browser in headless mode.",
@@ -118361,6 +118463,15 @@ var init_schemas = __esm({
118361
118463
  }
118362
118464
  }
118363
118465
  ]
118466
+ },
118467
+ browserFallback: {
118468
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
118469
+ type: "string",
118470
+ enum: [
118471
+ "auto",
118472
+ "explicit",
118473
+ "off"
118474
+ ]
118364
118475
  }
118365
118476
  },
118366
118477
  components: {
@@ -118403,6 +118514,11 @@ var init_schemas = __esm({
118403
118514
  ],
118404
118515
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
118405
118516
  },
118517
+ explicit: {
118518
+ type: "boolean",
118519
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
118520
+ readOnly: true
118521
+ },
118406
118522
  headless: {
118407
118523
  type: "boolean",
118408
118524
  description: "If `true`, runs the browser in headless mode.",
@@ -118852,6 +118968,11 @@ var init_schemas = __esm({
118852
118968
  ],
118853
118969
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
118854
118970
  },
118971
+ explicit: {
118972
+ type: "boolean",
118973
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
118974
+ readOnly: true
118975
+ },
118855
118976
  headless: {
118856
118977
  type: "boolean",
118857
118978
  description: "If `true`, runs the browser in headless mode.",
@@ -118926,6 +119047,11 @@ var init_schemas = __esm({
118926
119047
  ],
118927
119048
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
118928
119049
  },
119050
+ explicit: {
119051
+ type: "boolean",
119052
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
119053
+ readOnly: true
119054
+ },
118929
119055
  headless: {
118930
119056
  type: "boolean",
118931
119057
  description: "If `true`, runs the browser in headless mode.",
@@ -118970,6 +119096,15 @@ var init_schemas = __esm({
118970
119096
  }
118971
119097
  }
118972
119098
  ]
119099
+ },
119100
+ browserFallback: {
119101
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
119102
+ type: "string",
119103
+ enum: [
119104
+ "auto",
119105
+ "explicit",
119106
+ "off"
119107
+ ]
118973
119108
  }
118974
119109
  },
118975
119110
  components: {
@@ -119012,6 +119147,11 @@ var init_schemas = __esm({
119012
119147
  ],
119013
119148
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
119014
119149
  },
119150
+ explicit: {
119151
+ type: "boolean",
119152
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
119153
+ readOnly: true
119154
+ },
119015
119155
  headless: {
119016
119156
  type: "boolean",
119017
119157
  description: "If `true`, runs the browser in headless mode.",
@@ -147114,6 +147254,11 @@ var init_schemas = __esm({
147114
147254
  ],
147115
147255
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
147116
147256
  },
147257
+ explicit: {
147258
+ type: "boolean",
147259
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
147260
+ readOnly: true
147261
+ },
147117
147262
  headless: {
147118
147263
  type: "boolean",
147119
147264
  description: "If `true`, runs the browser in headless mode.",
@@ -175285,6 +175430,11 @@ var init_schemas = __esm({
175285
175430
  ],
175286
175431
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
175287
175432
  },
175433
+ explicit: {
175434
+ type: "boolean",
175435
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
175436
+ readOnly: true
175437
+ },
175288
175438
  headless: {
175289
175439
  type: "boolean",
175290
175440
  description: "If `true`, runs the browser in headless mode.",
@@ -175359,6 +175509,11 @@ var init_schemas = __esm({
175359
175509
  ],
175360
175510
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
175361
175511
  },
175512
+ explicit: {
175513
+ type: "boolean",
175514
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
175515
+ readOnly: true
175516
+ },
175362
175517
  headless: {
175363
175518
  type: "boolean",
175364
175519
  description: "If `true`, runs the browser in headless mode.",
@@ -175403,6 +175558,15 @@ var init_schemas = __esm({
175403
175558
  }
175404
175559
  }
175405
175560
  ]
175561
+ },
175562
+ browserFallback: {
175563
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
175564
+ type: "string",
175565
+ enum: [
175566
+ "auto",
175567
+ "explicit",
175568
+ "off"
175569
+ ]
175406
175570
  }
175407
175571
  },
175408
175572
  components: {
@@ -175445,6 +175609,11 @@ var init_schemas = __esm({
175445
175609
  ],
175446
175610
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
175447
175611
  },
175612
+ explicit: {
175613
+ type: "boolean",
175614
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
175615
+ readOnly: true
175616
+ },
175448
175617
  headless: {
175449
175618
  type: "boolean",
175450
175619
  description: "If `true`, runs the browser in headless mode.",
@@ -175894,6 +176063,11 @@ var init_schemas = __esm({
175894
176063
  ],
175895
176064
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
175896
176065
  },
176066
+ explicit: {
176067
+ type: "boolean",
176068
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
176069
+ readOnly: true
176070
+ },
175897
176071
  headless: {
175898
176072
  type: "boolean",
175899
176073
  description: "If `true`, runs the browser in headless mode.",
@@ -175968,6 +176142,11 @@ var init_schemas = __esm({
175968
176142
  ],
175969
176143
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
175970
176144
  },
176145
+ explicit: {
176146
+ type: "boolean",
176147
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
176148
+ readOnly: true
176149
+ },
175971
176150
  headless: {
175972
176151
  type: "boolean",
175973
176152
  description: "If `true`, runs the browser in headless mode.",
@@ -176012,6 +176191,15 @@ var init_schemas = __esm({
176012
176191
  }
176013
176192
  }
176014
176193
  ]
176194
+ },
176195
+ browserFallback: {
176196
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
176197
+ type: "string",
176198
+ enum: [
176199
+ "auto",
176200
+ "explicit",
176201
+ "off"
176202
+ ]
176015
176203
  }
176016
176204
  },
176017
176205
  components: {
@@ -176054,6 +176242,11 @@ var init_schemas = __esm({
176054
176242
  ],
176055
176243
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
176056
176244
  },
176245
+ explicit: {
176246
+ type: "boolean",
176247
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
176248
+ readOnly: true
176249
+ },
176057
176250
  headless: {
176058
176251
  type: "boolean",
176059
176252
  description: "If `true`, runs the browser in headless mode.",
@@ -204156,6 +204349,11 @@ var init_schemas = __esm({
204156
204349
  ],
204157
204350
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
204158
204351
  },
204352
+ explicit: {
204353
+ type: "boolean",
204354
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
204355
+ readOnly: true
204356
+ },
204159
204357
  headless: {
204160
204358
  type: "boolean",
204161
204359
  description: "If `true`, runs the browser in headless mode.",
@@ -234283,6 +234481,11 @@ var init_schemas = __esm({
234283
234481
  ],
234284
234482
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
234285
234483
  },
234484
+ explicit: {
234485
+ type: "boolean",
234486
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
234487
+ readOnly: true
234488
+ },
234286
234489
  headless: {
234287
234490
  type: "boolean",
234288
234491
  description: "If `true`, runs the browser in headless mode.",
@@ -234357,6 +234560,11 @@ var init_schemas = __esm({
234357
234560
  ],
234358
234561
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
234359
234562
  },
234563
+ explicit: {
234564
+ type: "boolean",
234565
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
234566
+ readOnly: true
234567
+ },
234360
234568
  headless: {
234361
234569
  type: "boolean",
234362
234570
  description: "If `true`, runs the browser in headless mode.",
@@ -234401,6 +234609,15 @@ var init_schemas = __esm({
234401
234609
  }
234402
234610
  }
234403
234611
  ]
234612
+ },
234613
+ browserFallback: {
234614
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
234615
+ type: "string",
234616
+ enum: [
234617
+ "auto",
234618
+ "explicit",
234619
+ "off"
234620
+ ]
234404
234621
  }
234405
234622
  },
234406
234623
  components: {
@@ -234443,6 +234660,11 @@ var init_schemas = __esm({
234443
234660
  ],
234444
234661
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
234445
234662
  },
234663
+ explicit: {
234664
+ type: "boolean",
234665
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
234666
+ readOnly: true
234667
+ },
234446
234668
  headless: {
234447
234669
  type: "boolean",
234448
234670
  description: "If `true`, runs the browser in headless mode.",
@@ -234892,6 +235114,11 @@ var init_schemas = __esm({
234892
235114
  ],
234893
235115
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
234894
235116
  },
235117
+ explicit: {
235118
+ type: "boolean",
235119
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
235120
+ readOnly: true
235121
+ },
234895
235122
  headless: {
234896
235123
  type: "boolean",
234897
235124
  description: "If `true`, runs the browser in headless mode.",
@@ -234966,6 +235193,11 @@ var init_schemas = __esm({
234966
235193
  ],
234967
235194
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
234968
235195
  },
235196
+ explicit: {
235197
+ type: "boolean",
235198
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
235199
+ readOnly: true
235200
+ },
234969
235201
  headless: {
234970
235202
  type: "boolean",
234971
235203
  description: "If `true`, runs the browser in headless mode.",
@@ -235010,6 +235242,15 @@ var init_schemas = __esm({
235010
235242
  }
235011
235243
  }
235012
235244
  ]
235245
+ },
235246
+ browserFallback: {
235247
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
235248
+ type: "string",
235249
+ enum: [
235250
+ "auto",
235251
+ "explicit",
235252
+ "off"
235253
+ ]
235013
235254
  }
235014
235255
  },
235015
235256
  components: {
@@ -235052,6 +235293,11 @@ var init_schemas = __esm({
235052
235293
  ],
235053
235294
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
235054
235295
  },
235296
+ explicit: {
235297
+ type: "boolean",
235298
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
235299
+ readOnly: true
235300
+ },
235055
235301
  headless: {
235056
235302
  type: "boolean",
235057
235303
  description: "If `true`, runs the browser in headless mode.",
@@ -263154,6 +263400,11 @@ var init_schemas = __esm({
263154
263400
  ],
263155
263401
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
263156
263402
  },
263403
+ explicit: {
263404
+ type: "boolean",
263405
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
263406
+ readOnly: true
263407
+ },
263157
263408
  headless: {
263158
263409
  type: "boolean",
263159
263410
  description: "If `true`, runs the browser in headless mode.",
@@ -318224,6 +318475,11 @@ var init_schemas = __esm({
318224
318475
  ],
318225
318476
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
318226
318477
  },
318478
+ explicit: {
318479
+ type: "boolean",
318480
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
318481
+ readOnly: true
318482
+ },
318227
318483
  headless: {
318228
318484
  type: "boolean",
318229
318485
  description: "If `true`, runs the browser in headless mode.",
@@ -318298,6 +318554,11 @@ var init_schemas = __esm({
318298
318554
  ],
318299
318555
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
318300
318556
  },
318557
+ explicit: {
318558
+ type: "boolean",
318559
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
318560
+ readOnly: true
318561
+ },
318301
318562
  headless: {
318302
318563
  type: "boolean",
318303
318564
  description: "If `true`, runs the browser in headless mode.",
@@ -318342,6 +318603,15 @@ var init_schemas = __esm({
318342
318603
  }
318343
318604
  }
318344
318605
  ]
318606
+ },
318607
+ browserFallback: {
318608
+ description: "Per-context override for the config-level [`browserFallback`](config) policy that governs whether a context whose browser can't start a driver session falls back to another available browser. Accepts the same values \u2014 `auto`, `explicit`, `off` \u2014 and, when set, takes precedence over the config-level value for the contexts this entry expands into. Omit it to inherit the config-level policy (which itself defaults to `auto`).",
318609
+ type: "string",
318610
+ enum: [
318611
+ "auto",
318612
+ "explicit",
318613
+ "off"
318614
+ ]
318345
318615
  }
318346
318616
  },
318347
318617
  components: {
@@ -318384,6 +318654,11 @@ var init_schemas = __esm({
318384
318654
  ],
318385
318655
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
318386
318656
  },
318657
+ explicit: {
318658
+ type: "boolean",
318659
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
318660
+ readOnly: true
318661
+ },
318387
318662
  headless: {
318388
318663
  type: "boolean",
318389
318664
  description: "If `true`, runs the browser in headless mode.",
@@ -346486,6 +346761,11 @@ var init_schemas = __esm({
346486
346761
  ],
346487
346762
  $comment: "`safari` is just a shortcut for `webkit`. Included for visibility and to reduce questions."
346488
346763
  },
346764
+ explicit: {
346765
+ type: "boolean",
346766
+ description: "Set automatically during context resolution: `true` when the author explicitly requested this browser (as opposed to it being auto-selected as the default). The runner's cross-browser fallback uses it to decide whether substituting another engine reports `PASS` (auto-selected) or `WARNING` (explicitly pinned).",
346767
+ readOnly: true
346768
+ },
346489
346769
  headless: {
346490
346770
  type: "boolean",
346491
346771
  description: "If `true`, runs the browser in headless mode.",
@@ -385247,16 +385527,410 @@ var init_utils = __esm({
385247
385527
  }
385248
385528
  });
385249
385529
 
385530
+ // dist/runtime/browsers.js
385531
+ var browsers_exports = {};
385532
+ __export(browsers_exports, {
385533
+ BROWSER_CHANNELS: () => BROWSER_CHANNELS,
385534
+ ensureBrowserInstalled: () => ensureBrowserInstalled,
385535
+ geckodriverBinaryInCache: () => geckodriverBinaryInCache,
385536
+ getInstalledBrowsers: () => getInstalledBrowsers,
385537
+ requiredBrowserAssets: () => requiredBrowserAssets,
385538
+ verifyDriverBinary: () => verifyDriverBinary
385539
+ });
385540
+ function isAllowedDriverPath(binaryPath) {
385541
+ return typeof binaryPath === "string" && import_node_path5.default.isAbsolute(binaryPath) && ALLOWED_DRIVER_PATH.test(binaryPath);
385542
+ }
385543
+ function parseDriverVersion(output) {
385544
+ const m = output.match(/(\d+\.\d+(?:\.\d+)*)/);
385545
+ return m ? m[1] : void 0;
385546
+ }
385547
+ async function verifyDriverBinary(driverName, binaryPath, options = {}) {
385548
+ if (!binaryPath || typeof binaryPath !== "string") {
385549
+ return { ok: false, error: "No driver binary path to verify." };
385550
+ }
385551
+ if (!isAllowedDriverPath(binaryPath)) {
385552
+ return {
385553
+ ok: false,
385554
+ error: `Refusing to execute '${binaryPath}': not a recognized driver binary path.`
385555
+ };
385556
+ }
385557
+ const key = String(driverName ?? "").toLowerCase();
385558
+ const args = DRIVER_VERSION_ARGS[key] ?? ["--version"];
385559
+ const exec = options.exec ?? defaultDriverExec;
385560
+ const timeoutMs = options.timeoutMs ?? DRIVER_VERIFY_TIMEOUT_MS;
385561
+ let res;
385562
+ try {
385563
+ res = await exec(binaryPath, args, timeoutMs);
385564
+ } catch (err) {
385565
+ return { ok: false, error: `Failed to execute ${driverName}: ${String(err)}` };
385566
+ }
385567
+ if (res.code === null) {
385568
+ const detail = (res.stderr || res.stdout || "").trim();
385569
+ return {
385570
+ ok: false,
385571
+ error: `${driverName} could not be executed (spawn failed)${detail ? `: ${detail}` : ""}`
385572
+ };
385573
+ }
385574
+ if (res.code !== 0) {
385575
+ const detail = (res.stderr || res.stdout || "").trim();
385576
+ return {
385577
+ ok: false,
385578
+ error: `${driverName} exited with code ${res.code}${detail ? `: ${detail}` : ""}`
385579
+ };
385580
+ }
385581
+ const version = parseDriverVersion(`${res.stdout}
385582
+ ${res.stderr}`);
385583
+ if (!version) {
385584
+ return {
385585
+ ok: false,
385586
+ error: `${driverName} ran but did not report a parseable version (likely a partial or corrupt download).`
385587
+ };
385588
+ }
385589
+ return { ok: true, version };
385590
+ }
385591
+ function requiredBrowserAssets(name) {
385592
+ switch ((name ?? "").toLowerCase()) {
385593
+ case "chrome":
385594
+ return ["chrome", "chromedriver"];
385595
+ case "firefox":
385596
+ return ["firefox", "geckodriver"];
385597
+ default:
385598
+ return [];
385599
+ }
385600
+ }
385601
+ async function loadPuppeteerBrowsers(deps, ctx) {
385602
+ if (deps.browsersModule)
385603
+ return deps.browsersModule;
385604
+ return await loadHeavyDep("@puppeteer/browsers", { ctx, deps: { logger: deps.logger } });
385605
+ }
385606
+ async function loadGeckodriver(deps, ctx) {
385607
+ if (deps.geckodriverModule)
385608
+ return deps.geckodriverModule;
385609
+ return await loadHeavyDep("geckodriver", { ctx, deps: { logger: deps.logger } });
385610
+ }
385611
+ function isStillFresh(latestCheckedAt, now) {
385612
+ if (!latestCheckedAt)
385613
+ return false;
385614
+ const t = Date.parse(latestCheckedAt);
385615
+ if (Number.isNaN(t))
385616
+ return false;
385617
+ return now.getTime() - t < FRESHNESS_TTL_MS;
385618
+ }
385619
+ async function resolveChannelBuildId(browsersModule, assetName, platform) {
385620
+ const channel = BROWSER_CHANNELS[assetName];
385621
+ return await withTimeout(Promise.resolve(browsersModule.resolveBuildId(assetName, platform, channel)), RESOLVE_TIMEOUT_MS, `resolveBuildId(${assetName}, ${channel})`);
385622
+ }
385623
+ function withTimeout(p, ms, label) {
385624
+ return new Promise((resolve, reject) => {
385625
+ const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
385626
+ if (typeof t.unref === "function")
385627
+ t.unref();
385628
+ p.then((v) => {
385629
+ clearTimeout(t);
385630
+ resolve(v);
385631
+ }, (err) => {
385632
+ clearTimeout(t);
385633
+ reject(err);
385634
+ });
385635
+ });
385636
+ }
385637
+ async function ensureBrowserInstalled(name, options = {}) {
385638
+ const { ctx = {} } = options;
385639
+ const dedupeKey = `${name}:${getBrowsersDir(ctx)}`;
385640
+ const inFlight = inFlightInstalls.get(dedupeKey);
385641
+ if (inFlight)
385642
+ return inFlight;
385643
+ const p = ensureBrowserInstalledImpl(name, options).finally(() => {
385644
+ inFlightInstalls.delete(dedupeKey);
385645
+ });
385646
+ inFlightInstalls.set(dedupeKey, p);
385647
+ return p;
385648
+ }
385649
+ async function ensureBrowserInstalledImpl(name, options = {}) {
385650
+ const { ctx = {}, deps = {}, force = false } = options;
385651
+ const logger = deps.logger ?? defaultLogger2;
385652
+ const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
385653
+ const cacheDir = getBrowsersDir(ctx);
385654
+ const record = readInstalledRecord(ctx);
385655
+ const existing = record.browsers[name];
385656
+ if (name === "geckodriver") {
385657
+ return await ensureGeckodriver({ ctx, deps: { ...deps, logger }, force, now }, record, existing, logger);
385658
+ }
385659
+ const browsersModule = await loadPuppeteerBrowsers(deps, ctx);
385660
+ const platform = browsersModule.detectBrowserPlatform ? browsersModule.detectBrowserPlatform() : void 0;
385661
+ if (!platform) {
385662
+ throw new Error(`Unable to determine browser platform on ${process.platform}/${process.arch}. @puppeteer/browsers does not recognize this OS; ${name} cannot be installed automatically. Set DOC_DETECTIVE_CACHE_DIR to a pre-warmed cache or skip browser-driven steps.`);
385663
+ }
385664
+ if (!force && existing && isStillFresh(existing.latestCheckedAt, now)) {
385665
+ if (existing.latestKnownVersion === existing.installedVersion) {
385666
+ const path24 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
385667
+ return { path: path24, version: existing.installedVersion, outdated: false };
385668
+ }
385669
+ const path23 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
385670
+ 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");
385671
+ return { path: path23, version: existing.installedVersion, outdated: true };
385672
+ }
385673
+ let latest;
385674
+ try {
385675
+ latest = await resolveChannelBuildId(browsersModule, name, platform);
385676
+ } catch (err) {
385677
+ logger(`Channel resolution for ${name} skipped: ${String(err)}`, "debug");
385678
+ if (existing) {
385679
+ const path23 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
385680
+ return { path: path23, version: existing.installedVersion, outdated: false };
385681
+ }
385682
+ throw err;
385683
+ }
385684
+ if (!force && existing && existing.installedVersion === latest) {
385685
+ record.browsers[name] = {
385686
+ ...existing,
385687
+ latestKnownVersion: latest,
385688
+ latestCheckedAt: now.toISOString()
385689
+ };
385690
+ writeInstalledRecord(record, ctx);
385691
+ const path23 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
385692
+ return { path: path23, version: latest, outdated: false };
385693
+ }
385694
+ if (!force && existing) {
385695
+ record.browsers[name] = {
385696
+ ...existing,
385697
+ latestKnownVersion: latest,
385698
+ latestCheckedAt: now.toISOString()
385699
+ };
385700
+ writeInstalledRecord(record, ctx);
385701
+ logger(`${name} ${existing.installedVersion} installed in ${cacheDir}; "${BROWSER_CHANNELS[name]}" channel is now ${latest}. Run \`doc-detective install browsers ${name} --force\` to update.`, "warn");
385702
+ const path23 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
385703
+ return { path: path23, version: existing.installedVersion, outdated: true };
385704
+ }
385705
+ logger(`Installing ${name} ${latest} into ${cacheDir}`, "info");
385706
+ await browsersModule.install({
385707
+ browser: name,
385708
+ buildId: latest,
385709
+ cacheDir
385710
+ });
385711
+ let path22 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
385712
+ if (name === "chromedriver" && isAllowedDriverPath(path22)) {
385713
+ let verify = await verifyDriverBinary(name, path22, { exec: deps.verifyExec });
385714
+ if (!verify.ok) {
385715
+ logger(`Installed ${name} ${latest} failed validation (${verify.error}); reinstalling once.`, "warn");
385716
+ try {
385717
+ await browsersModule.uninstall({ browser: name, buildId: latest, cacheDir });
385718
+ } catch (err) {
385719
+ logger(`Failed to prune broken ${name} ${latest}: ${String(err)}`, "debug");
385720
+ }
385721
+ await browsersModule.install({ browser: name, buildId: latest, cacheDir });
385722
+ path22 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
385723
+ verify = await verifyDriverBinary(name, path22, { exec: deps.verifyExec });
385724
+ }
385725
+ if (!verify.ok) {
385726
+ throw new Error(`${name} ${latest} is present but non-functional after a reinstall (${verify.error}). It may be a partial or corrupt download; delete ${path22} or reinstall.`);
385727
+ }
385728
+ }
385729
+ if (existing && existing.installedVersion !== latest) {
385730
+ try {
385731
+ await browsersModule.uninstall({
385732
+ browser: name,
385733
+ buildId: existing.installedVersion,
385734
+ cacheDir
385735
+ });
385736
+ } catch (err) {
385737
+ logger(`Failed to prune old ${name} buildId ${existing.installedVersion}: ${String(err)}`, "debug");
385738
+ }
385739
+ }
385740
+ record.browsers[name] = {
385741
+ installedVersion: latest,
385742
+ installedAt: now.toISOString(),
385743
+ latestKnownVersion: latest,
385744
+ latestCheckedAt: now.toISOString()
385745
+ };
385746
+ writeInstalledRecord(record, ctx);
385747
+ return { path: path22, version: latest, outdated: false };
385748
+ }
385749
+ async function locateExecutable(browsersModule, name, buildId, cacheDir, platform) {
385750
+ if (typeof browsersModule.computeExecutablePath === "function") {
385751
+ return browsersModule.computeExecutablePath({
385752
+ browser: name,
385753
+ buildId,
385754
+ cacheDir,
385755
+ platform
385756
+ });
385757
+ }
385758
+ return cacheDir;
385759
+ }
385760
+ async function withGeckodriverCacheDir(cacheDir, fn) {
385761
+ const previous = geckodriverEnvQueue;
385762
+ let release;
385763
+ geckodriverEnvQueue = new Promise((resolve) => {
385764
+ release = resolve;
385765
+ });
385766
+ await previous;
385767
+ const prevEnv = process.env.GECKODRIVER_CACHE_DIR;
385768
+ process.env.GECKODRIVER_CACHE_DIR = cacheDir;
385769
+ try {
385770
+ return await fn();
385771
+ } finally {
385772
+ if (prevEnv === void 0)
385773
+ delete process.env.GECKODRIVER_CACHE_DIR;
385774
+ else
385775
+ process.env.GECKODRIVER_CACHE_DIR = prevEnv;
385776
+ release();
385777
+ }
385778
+ }
385779
+ function isPathInside(child, dir) {
385780
+ const resolvedDir = import_node_path5.default.resolve(dir);
385781
+ const resolvedChild = import_node_path5.default.resolve(child);
385782
+ return resolvedChild === resolvedDir || resolvedChild.startsWith(resolvedDir + import_node_path5.default.sep);
385783
+ }
385784
+ function geckodriverBinaryInCache(cacheDir) {
385785
+ const binName = process.platform === "win32" ? "geckodriver.exe" : "geckodriver";
385786
+ const rootCandidate = import_node_path5.default.join(cacheDir, binName);
385787
+ try {
385788
+ if (import_node_fs5.default.existsSync(rootCandidate))
385789
+ return rootCandidate;
385790
+ } catch {
385791
+ }
385792
+ try {
385793
+ for (const entry of import_node_fs5.default.readdirSync(cacheDir, { withFileTypes: true })) {
385794
+ if (!entry.isDirectory())
385795
+ continue;
385796
+ const nested = import_node_path5.default.join(cacheDir, entry.name, binName);
385797
+ if (import_node_fs5.default.existsSync(nested))
385798
+ return nested;
385799
+ }
385800
+ } catch {
385801
+ }
385802
+ return void 0;
385803
+ }
385804
+ async function ensureGeckodriver(ctxBag, record, existing, logger) {
385805
+ const cacheDir = getBrowsersDir(ctxBag.ctx);
385806
+ const resolveBinaryPath = (gecko) => {
385807
+ const fromModule = gecko && typeof gecko.path === "string" && gecko.path.length > 0 && isPathInside(gecko.path, cacheDir) ? gecko.path : null;
385808
+ return fromModule ?? geckodriverBinaryInCache(cacheDir) ?? cacheDir;
385809
+ };
385810
+ if (!ctxBag.force && existing && isStillFresh(existing.latestCheckedAt, ctxBag.now)) {
385811
+ const gecko = ctxBag.deps.geckodriverModule ?? await withGeckodriverCacheDir(cacheDir, () => loadGeckodriver(ctxBag.deps, ctxBag.ctx));
385812
+ return {
385813
+ path: resolveBinaryPath(gecko),
385814
+ version: existing.installedVersion,
385815
+ // Only mark outdated when we actually KNOW a different latest
385816
+ // version exists. If `latestKnownVersion` is undefined (e.g., the
385817
+ // installer couldn't determine a version and we recorded
385818
+ // `installedVersion: "unknown"` with `latestKnownVersion: undefined`),
385819
+ // a naive `!==` comparison would flag the entry as outdated on
385820
+ // every run.
385821
+ outdated: typeof existing.latestKnownVersion === "string" && existing.latestKnownVersion.length > 0 && existing.latestKnownVersion !== existing.installedVersion
385822
+ };
385823
+ }
385824
+ return await withGeckodriverCacheDir(cacheDir, async () => {
385825
+ const gecko = await loadGeckodriver(ctxBag.deps, ctxBag.ctx);
385826
+ logger(`Installing geckodriver into ${cacheDir}`, "info");
385827
+ await gecko.download();
385828
+ let binaryPath = resolveBinaryPath(gecko);
385829
+ let verify = await verifyDriverBinary("geckodriver", binaryPath, {
385830
+ exec: ctxBag.deps.verifyExec
385831
+ });
385832
+ if (!verify.ok) {
385833
+ logger(`Downloaded geckodriver failed validation (${verify.error}); re-downloading once.`, "warn");
385834
+ try {
385835
+ if (binaryPath && binaryPath !== cacheDir && import_node_fs5.default.existsSync(binaryPath)) {
385836
+ import_node_fs5.default.rmSync(binaryPath, { force: true });
385837
+ }
385838
+ } catch {
385839
+ }
385840
+ await gecko.download();
385841
+ binaryPath = resolveBinaryPath(gecko);
385842
+ verify = await verifyDriverBinary("geckodriver", binaryPath, {
385843
+ exec: ctxBag.deps.verifyExec
385844
+ });
385845
+ }
385846
+ if (!verify.ok) {
385847
+ throw new Error(`geckodriver is present but non-functional after a re-download (${verify.error}). It may be a partial or corrupt download; delete ${binaryPath} or reinstall.`);
385848
+ }
385849
+ const installedVersion = verify.version;
385850
+ record.browsers.geckodriver = {
385851
+ installedVersion,
385852
+ installedAt: ctxBag.now.toISOString(),
385853
+ latestKnownVersion: installedVersion,
385854
+ latestCheckedAt: ctxBag.now.toISOString()
385855
+ };
385856
+ writeInstalledRecord(record, ctxBag.ctx);
385857
+ return {
385858
+ path: binaryPath,
385859
+ version: installedVersion,
385860
+ outdated: false
385861
+ };
385862
+ });
385863
+ }
385864
+ function getInstalledBrowsers(ctx = {}) {
385865
+ const record = readInstalledRecord(ctx);
385866
+ return Object.entries(record.browsers).map(([name, entry]) => ({
385867
+ name,
385868
+ installedVersion: entry.installedVersion,
385869
+ installedAt: entry.installedAt,
385870
+ latestKnownVersion: entry.latestKnownVersion
385871
+ }));
385872
+ }
385873
+ var import_node_child_process3, import_node_fs5, import_node_path5, DRIVER_VERSION_ARGS, DRIVER_VERIFY_TIMEOUT_MS, ALLOWED_DRIVER_PATH, defaultDriverExec, BROWSER_CHANNELS, FRESHNESS_TTL_MS, RESOLVE_TIMEOUT_MS, RUNTIME_DEBUG2, defaultLogger2, inFlightInstalls, geckodriverEnvQueue;
385874
+ var init_browsers = __esm({
385875
+ "dist/runtime/browsers.js"() {
385876
+ "use strict";
385877
+ import_node_child_process3 = require("node:child_process");
385878
+ import_node_fs5 = __toESM(require("node:fs"), 1);
385879
+ import_node_path5 = __toESM(require("node:path"), 1);
385880
+ init_cacheDir();
385881
+ init_loader();
385882
+ DRIVER_VERSION_ARGS = {
385883
+ geckodriver: ["--version"],
385884
+ chromedriver: ["--version"],
385885
+ safaridriver: ["--version"]
385886
+ };
385887
+ DRIVER_VERIFY_TIMEOUT_MS = 1e4;
385888
+ ALLOWED_DRIVER_PATH = /[\\/](?:geckodriver|chromedriver|safaridriver)(?:\.exe)?$/i;
385889
+ defaultDriverExec = (binaryPath, args, timeoutMs) => new Promise((resolve) => {
385890
+ (0, import_node_child_process3.execFile)(binaryPath, args, { timeout: timeoutMs, windowsHide: true }, (err, stdout, stderr) => {
385891
+ const out = typeof stdout === "string" ? stdout : String(stdout ?? "");
385892
+ const errOut = typeof stderr === "string" ? stderr : String(stderr ?? "");
385893
+ if (err && typeof err.code === "number") {
385894
+ resolve({ code: err.code, stdout: out, stderr: errOut });
385895
+ } else if (err) {
385896
+ resolve({ code: null, stdout: out, stderr: `${errOut}${String(err)}` });
385897
+ } else {
385898
+ resolve({ code: 0, stdout: out, stderr: errOut });
385899
+ }
385900
+ });
385901
+ });
385902
+ BROWSER_CHANNELS = {
385903
+ chrome: "stable",
385904
+ firefox: "latest",
385905
+ chromedriver: "stable",
385906
+ geckodriver: "latest"
385907
+ };
385908
+ FRESHNESS_TTL_MS = 24 * 60 * 60 * 1e3;
385909
+ RESOLVE_TIMEOUT_MS = 5e3;
385910
+ RUNTIME_DEBUG2 = process.env.DOC_DETECTIVE_RUNTIME_DEBUG === "1";
385911
+ defaultLogger2 = (msg, level = "info") => {
385912
+ if (level === "debug" && !RUNTIME_DEBUG2)
385913
+ return;
385914
+ if (level === "error")
385915
+ console.error(msg);
385916
+ else
385917
+ console.log(msg);
385918
+ };
385919
+ inFlightInstalls = /* @__PURE__ */ new Map();
385920
+ geckodriverEnvQueue = Promise.resolve();
385921
+ }
385922
+ });
385923
+
385250
385924
  // dist/core/appium.js
385251
385925
  function appiumHomeForDriverPath(driverEntry) {
385252
- const marker = `${import_node_path5.default.sep}node_modules${import_node_path5.default.sep}`;
385926
+ const marker = `${import_node_path6.default.sep}node_modules${import_node_path6.default.sep}`;
385253
385927
  const idx = driverEntry.lastIndexOf(marker);
385254
385928
  return idx === -1 ? null : driverEntry.slice(0, idx);
385255
385929
  }
385256
385930
  function setAppiumHome(ctx = {}) {
385257
- const runtimeAppium = import_node_path5.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }), "node_modules", "appium");
385258
- if ((0, import_node_fs5.existsSync)(runtimeAppium)) {
385259
- process.env.APPIUM_HOME = import_node_path5.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }));
385931
+ const runtimeAppium = import_node_path6.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }), "node_modules", "appium");
385932
+ if ((0, import_node_fs6.existsSync)(runtimeAppium)) {
385933
+ process.env.APPIUM_HOME = import_node_path6.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }));
385260
385934
  return;
385261
385935
  }
385262
385936
  for (const driverName of ["appium-chromium-driver", "appium-geckodriver"]) {
@@ -385269,27 +385943,27 @@ function setAppiumHome(ctx = {}) {
385269
385943
  return;
385270
385944
  }
385271
385945
  }
385272
- const corePath = import_node_path5.default.join(__dirname3, "../../node_modules");
385946
+ const corePath = import_node_path6.default.join(__dirname3, "../../node_modules");
385273
385947
  const pathArray = corePath.split("node_modules");
385274
385948
  let appiumParentPath = pathArray[0];
385275
385949
  for (let i = 1; i < pathArray.length; i++) {
385276
- if ((0, import_node_fs5.existsSync)(import_node_path5.default.join(appiumParentPath, "node_modules", "appium"))) {
385950
+ if ((0, import_node_fs6.existsSync)(import_node_path6.default.join(appiumParentPath, "node_modules", "appium"))) {
385277
385951
  break;
385278
385952
  }
385279
- appiumParentPath = import_node_path5.default.join(appiumParentPath, "node_modules", pathArray[i]);
385953
+ appiumParentPath = import_node_path6.default.join(appiumParentPath, "node_modules", pathArray[i]);
385280
385954
  }
385281
385955
  process.env.APPIUM_HOME = appiumParentPath;
385282
385956
  }
385283
- var import_node_path5, import_node_fs5, import_node_url4, __dirname3;
385957
+ var import_node_path6, import_node_fs6, import_node_url4, __dirname3;
385284
385958
  var init_appium = __esm({
385285
385959
  "dist/core/appium.js"() {
385286
385960
  "use strict";
385287
- import_node_path5 = __toESM(require("node:path"), 1);
385288
- import_node_fs5 = require("node:fs");
385961
+ import_node_path6 = __toESM(require("node:path"), 1);
385962
+ import_node_fs6 = require("node:fs");
385289
385963
  import_node_url4 = require("node:url");
385290
385964
  init_cacheDir();
385291
385965
  init_loader();
385292
- __dirname3 = import_node_path5.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
385966
+ __dirname3 = import_node_path6.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
385293
385967
  }
385294
385968
  });
385295
385969
 
@@ -385321,7 +385995,7 @@ async function readFile({ fileURLOrPath }) {
385321
385995
  }
385322
385996
  } else {
385323
385997
  try {
385324
- content = await import_node_fs6.default.promises.readFile(fileURLOrPath, "utf8");
385998
+ content = await import_node_fs7.default.promises.readFile(fileURLOrPath, "utf8");
385325
385999
  } catch (error) {
385326
386000
  if (error.code === "ENOENT") {
385327
386001
  console.warn(`File not found: ${fileURLOrPath}`);
@@ -385392,13 +386066,13 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
385392
386066
  if (relativePath.startsWith("https://") || relativePath.startsWith("http://") || relativePath.startsWith("heretto:")) {
385393
386067
  return relativePath;
385394
386068
  }
385395
- if (import_node_path6.default.isAbsolute(relativePath)) {
386069
+ if (import_node_path7.default.isAbsolute(relativePath)) {
385396
386070
  return relativePath;
385397
386071
  }
385398
- const fileExists = import_node_fs6.default.existsSync(filePath2);
385399
- const isFile = fileExists ? import_node_fs6.default.lstatSync(filePath2).isFile() : import_node_path6.default.parse(filePath2).ext !== "";
385400
- const basePath = isFile ? import_node_path6.default.dirname(filePath2) : filePath2;
385401
- return baseType === "file" ? import_node_path6.default.resolve(basePath, relativePath) : import_node_path6.default.resolve(relativePath);
386072
+ const fileExists = import_node_fs7.default.existsSync(filePath2);
386073
+ const isFile = fileExists ? import_node_fs7.default.lstatSync(filePath2).isFile() : import_node_path7.default.parse(filePath2).ext !== "";
386074
+ const basePath = isFile ? import_node_path7.default.dirname(filePath2) : filePath2;
386075
+ return baseType === "file" ? import_node_path7.default.resolve(basePath, relativePath) : import_node_path7.default.resolve(relativePath);
385402
386076
  }
385403
386077
  const relativePathBase = config.relativePathBase;
385404
386078
  let pathProperties;
@@ -385447,7 +386121,7 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
385447
386121
  objectType
385448
386122
  });
385449
386123
  } else if (typeof item === "string" && pathProperties.includes(property)) {
385450
- const resolved = property === "path" && object.directory && import_node_path6.default.isAbsolute(object.directory) ? resolve(relativePathBase, item, object.directory) : resolve(relativePathBase, item, filePath);
386124
+ const resolved = property === "path" && object.directory && import_node_path7.default.isAbsolute(object.directory) ? resolve(relativePathBase, item, object.directory) : resolve(relativePathBase, item, filePath);
385451
386125
  object[property][i] = resolved;
385452
386126
  }
385453
386127
  }
@@ -385465,7 +386139,7 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
385465
386139
  }
385466
386140
  if (pathProperties.includes(property)) {
385467
386141
  if (property === "path" && object.directory) {
385468
- const directory = import_node_path6.default.isAbsolute(object.directory) ? object.directory : resolve(relativePathBase, object.directory, filePath);
386142
+ const directory = import_node_path7.default.isAbsolute(object.directory) ? object.directory : resolve(relativePathBase, object.directory, filePath);
385469
386143
  object[property] = resolve(relativePathBase, object[property], directory);
385470
386144
  } else {
385471
386145
  object[property] = resolve(relativePathBase, object[property], filePath);
@@ -385475,12 +386149,12 @@ async function resolvePaths({ config, object, filePath, nested = false, objectTy
385475
386149
  }
385476
386150
  return object;
385477
386151
  }
385478
- var import_node_fs6, import_node_path6, import_yaml, import_axios2;
386152
+ var import_node_fs7, import_node_path7, import_yaml, import_axios2;
385479
386153
  var init_files = __esm({
385480
386154
  "dist/core/files.js"() {
385481
386155
  "use strict";
385482
- import_node_fs6 = __toESM(require("node:fs"), 1);
385483
- import_node_path6 = __toESM(require("node:path"), 1);
386156
+ import_node_fs7 = __toESM(require("node:fs"), 1);
386157
+ import_node_path7 = __toESM(require("node:path"), 1);
385484
386158
  import_yaml = __toESM(require("yaml"), 1);
385485
386159
  import_axios2 = __toESM(require("axios"), 1);
385486
386160
  init_validate();
@@ -385503,10 +386177,10 @@ async function getOperation(definition = {}, operationId = "", responseCode = ""
385503
386177
  if (!operationId) {
385504
386178
  throw new Error("OperationId is required.");
385505
386179
  }
385506
- for (const path21 in definition.paths) {
385507
- for (const method in definition.paths[path21]) {
385508
- if (definition.paths[path21][method].operationId === operationId) {
385509
- const operation = definition.paths[path21][method];
386180
+ for (const path22 in definition.paths) {
386181
+ for (const method in definition.paths[path22]) {
386182
+ if (definition.paths[path22][method].operationId === operationId) {
386183
+ const operation = definition.paths[path22][method];
385510
386184
  if (!server) {
385511
386185
  if (definition.servers && definition.servers.length > 0) {
385512
386186
  server = definition.servers[0].url;
@@ -385514,9 +386188,9 @@ async function getOperation(definition = {}, operationId = "", responseCode = ""
385514
386188
  throw new Error("No server URL provided and no servers defined in the OpenAPI definition.");
385515
386189
  }
385516
386190
  }
385517
- const example = await compileExample(operation, server + path21, responseCode, exampleKey);
386191
+ const example = await compileExample(operation, server + path22, responseCode, exampleKey);
385518
386192
  const schemas2 = getSchemas(operation, responseCode);
385519
- return { path: path21, method, definition: operation, schemas: schemas2, example };
386193
+ return { path: path22, method, definition: operation, schemas: schemas2, example };
385520
386194
  }
385521
386195
  }
385522
386196
  }
@@ -385537,15 +386211,15 @@ function getSchemas(definition = {}, responseCode = "") {
385537
386211
  schemas2.response = definition.responses[responseCode].content[Object.keys(definition.responses[responseCode].content)[0]].schema;
385538
386212
  return schemas2;
385539
386213
  }
385540
- async function compileExample(operation = {}, path21 = "", responseCode = "", exampleKey = "") {
386214
+ async function compileExample(operation = {}, path22 = "", responseCode = "", exampleKey = "") {
385541
386215
  if (!operation) {
385542
386216
  throw new Error("Operation is required.");
385543
386217
  }
385544
- if (!path21) {
386218
+ if (!path22) {
385545
386219
  throw new Error("Path is required.");
385546
386220
  }
385547
386221
  let example = {
385548
- url: path21,
386222
+ url: path22,
385549
386223
  request: { parameters: {}, headers: {}, body: {} },
385550
386224
  response: { headers: {}, body: {} }
385551
386225
  };
@@ -385712,8 +386386,25 @@ __export(config_exports, {
385712
386386
  getBrowserDiagnostics: () => getBrowserDiagnostics,
385713
386387
  getEnvironment: () => getEnvironment,
385714
386388
  resolveConcurrentRunners: () => resolveConcurrentRunners,
385715
- setConfig: () => setConfig
386389
+ setConfig: () => setConfig,
386390
+ verifyAppDrivers: () => verifyAppDrivers
385716
386391
  });
386392
+ async function verifyAppDrivers(descriptors, { verify, logger }) {
386393
+ const out = [];
386394
+ for (const d of descriptors) {
386395
+ if (!d.driverName || !d.driverPath) {
386396
+ out.push(d.app);
386397
+ continue;
386398
+ }
386399
+ const res = await verify(d.driverName, d.driverPath);
386400
+ if (res.ok) {
386401
+ out.push(d.app);
386402
+ } else if (logger) {
386403
+ logger(`Excluding ${d.app?.name ?? d.driverName} from available browsers: its ${d.driverName} driver is present but did not validate (${res.error ?? "no error reported"}). Possible causes include a partial or corrupt download, a permissions issue, or a missing dependency.`, "warning");
386404
+ }
386405
+ }
386406
+ return out;
386407
+ }
385717
386408
  function deepMerge(target, override) {
385718
386409
  const result = { ...target };
385719
386410
  for (const key in override) {
@@ -385907,7 +386598,7 @@ function clearAppCache(config) {
385907
386598
  async function probeBrowserEnvironment({ config, browsersDir }) {
385908
386599
  setAppiumHome({ cacheDir: config?.cacheDir });
385909
386600
  const cwd = process.cwd();
385910
- process.chdir(import_node_path7.default.join(__dirname4, "../.."));
386601
+ process.chdir(import_node_path8.default.join(__dirname4, "../.."));
385911
386602
  let installedBrowsers = [];
385912
386603
  let browserDetectionFailed = false;
385913
386604
  let appiumDriverOutput = "";
@@ -385942,7 +386633,7 @@ async function probeBrowserEnvironment({ config, browsersDir }) {
385942
386633
  });
385943
386634
  return;
385944
386635
  }
385945
- const child = (0, import_node_child_process3.spawn)(process.execPath, [appiumEntry, "driver", "list"], { env: process.env });
386636
+ const child = (0, import_node_child_process4.spawn)(process.execPath, [appiumEntry, "driver", "list"], { env: process.env });
385946
386637
  let stdout = "";
385947
386638
  let stderr = "";
385948
386639
  child.stdout?.on("data", (c) => {
@@ -385979,44 +386670,69 @@ async function getAvailableApps({ config }) {
385979
386670
  if (hit)
385980
386671
  return hit;
385981
386672
  const { installedBrowsers, appiumDriverOutput, browserDetectionFailed } = await probeBrowserEnvironment({ config, browsersDir });
385982
- const apps = [];
386673
+ const descriptors = [];
385983
386674
  const chrome = installedBrowsers.find((browser) => browser.browser === "chrome");
385984
386675
  const chromeVersion = chrome?.buildId;
385985
386676
  const chromedriver = installedBrowsers.find((browser) => browser.browser === "chromedriver");
385986
386677
  const appiumChromium = appiumDriverOutput.match(/\n.*chromium.*installed \(npm\).*\n/);
385987
386678
  if (chrome && chromedriver && appiumChromium) {
385988
- apps.push({
385989
- name: "chrome",
385990
- version: chromeVersion,
385991
- path: chrome.executablePath,
385992
- driver: chromedriver.executablePath
386679
+ descriptors.push({
386680
+ app: {
386681
+ name: "chrome",
386682
+ version: chromeVersion,
386683
+ path: chrome.executablePath,
386684
+ driver: chromedriver.executablePath
386685
+ },
386686
+ driverName: "chromedriver",
386687
+ driverPath: chromedriver.executablePath
385993
386688
  });
385994
386689
  }
385995
386690
  const firefox = installedBrowsers.find((browser) => browser.browser === "firefox");
385996
386691
  const appiumFirefox = appiumDriverOutput.match(/\n.*gecko.*installed \(npm\).*\n/);
385997
386692
  if (firefox && appiumFirefox) {
385998
- apps.push({
385999
- name: "firefox",
386000
- version: firefox.buildId,
386001
- path: firefox.executablePath
386693
+ const geckodriverPath = resolveGeckodriverBinaryPath(config, browsersDir);
386694
+ descriptors.push({
386695
+ app: {
386696
+ name: "firefox",
386697
+ version: firefox.buildId,
386698
+ path: firefox.executablePath
386699
+ },
386700
+ driverName: geckodriverPath ? "geckodriver" : void 0,
386701
+ driverPath: geckodriverPath
386002
386702
  });
386003
386703
  }
386004
386704
  if (config.environment.platform === "mac") {
386005
386705
  const safariVersion = await spawnCommand("defaults read /Applications/Safari.app/Contents/Info.plist CFBundleShortVersionString");
386006
386706
  const appiumSafari = appiumDriverOutput.match(/\n.*safari.*installed \(npm\).*\n/);
386007
386707
  if (safariVersion.exitCode === 0 && appiumSafari) {
386008
- apps.push({ name: "safari", version: safariVersion.stdout.trim(), path: "" });
386708
+ descriptors.push({
386709
+ app: { name: "safari", version: safariVersion.stdout.trim(), path: "" },
386710
+ driverName: "safaridriver",
386711
+ driverPath: "/usr/bin/safaridriver"
386712
+ });
386009
386713
  }
386010
386714
  }
386715
+ const apps = await verifyAppDrivers(descriptors, {
386716
+ verify: (driverName, driverPath) => verifyDriverBinary(driverName, driverPath),
386717
+ logger: (msg, level) => log(config, level ?? "warning", msg)
386718
+ });
386011
386719
  if (!browserDetectionFailed)
386012
386720
  cachedAppsByDir.set(key, apps);
386013
386721
  return apps;
386014
386722
  }
386723
+ function resolveGeckodriverBinaryPath(config, browsersDir) {
386724
+ const installed = resolveHeavyDepPath("geckodriver", {
386725
+ cacheDir: config?.cacheDir
386726
+ });
386727
+ if (!installed)
386728
+ return void 0;
386729
+ return geckodriverBinaryInCache(browsersDir);
386730
+ }
386015
386731
  function probeSafariVersion(timeoutMs) {
386016
386732
  return new Promise((resolve) => {
386017
386733
  let child;
386018
386734
  try {
386019
- child = (0, import_node_child_process3.spawn)("defaults", [
386735
+ child = (0, import_node_child_process4.spawn)("defaults", [
386020
386736
  "read",
386021
386737
  "/Applications/Safari.app/Contents/Info.plist",
386022
386738
  "CFBundleShortVersionString"
@@ -386097,7 +386813,7 @@ async function getBrowserDiagnostics({ config }) {
386097
386813
  const version = await probeSafariVersion(4e3);
386098
386814
  safariApp = version !== null;
386099
386815
  safariVersion = version ?? void 0;
386100
- safaridriver = import_node_fs7.default.existsSync("/usr/bin/safaridriver");
386816
+ safaridriver = import_node_fs8.default.existsSync("/usr/bin/safaridriver");
386101
386817
  } catch {
386102
386818
  detectionFailed = true;
386103
386819
  }
@@ -386119,22 +386835,23 @@ async function getBrowserDiagnostics({ config }) {
386119
386835
  });
386120
386836
  return { browsers, detectionFailed };
386121
386837
  }
386122
- var import_node_os3, import_node_fs7, import_node_path7, import_node_child_process3, import_node_url5, __dirname4, platformMap, defaultFileTypes, cachedAppsByDir;
386838
+ var import_node_os3, import_node_fs8, import_node_path8, import_node_child_process4, import_node_url5, __dirname4, platformMap, defaultFileTypes, cachedAppsByDir;
386123
386839
  var init_config = __esm({
386124
386840
  "dist/core/config.js"() {
386125
386841
  "use strict";
386126
386842
  import_node_os3 = __toESM(require("node:os"), 1);
386127
- import_node_fs7 = __toESM(require("node:fs"), 1);
386843
+ import_node_fs8 = __toESM(require("node:fs"), 1);
386128
386844
  init_validate();
386129
386845
  init_utils();
386130
- import_node_path7 = __toESM(require("node:path"), 1);
386131
- import_node_child_process3 = require("node:child_process");
386846
+ import_node_path8 = __toESM(require("node:path"), 1);
386847
+ import_node_child_process4 = require("node:child_process");
386132
386848
  init_loader();
386133
386849
  init_cacheDir();
386850
+ init_browsers();
386134
386851
  init_appium();
386135
386852
  init_openapi();
386136
386853
  import_node_url5 = require("node:url");
386137
- __dirname4 = import_node_path7.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
386854
+ __dirname4 = import_node_path8.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
386138
386855
  platformMap = {
386139
386856
  darwin: "mac",
386140
386857
  linux: "linux",
@@ -386436,252 +387153,6 @@ var init_browserStepKeys = __esm({
386436
387153
  }
386437
387154
  });
386438
387155
 
386439
- // dist/runtime/browsers.js
386440
- var browsers_exports = {};
386441
- __export(browsers_exports, {
386442
- BROWSER_CHANNELS: () => BROWSER_CHANNELS,
386443
- ensureBrowserInstalled: () => ensureBrowserInstalled,
386444
- getInstalledBrowsers: () => getInstalledBrowsers,
386445
- requiredBrowserAssets: () => requiredBrowserAssets
386446
- });
386447
- function requiredBrowserAssets(name) {
386448
- switch ((name ?? "").toLowerCase()) {
386449
- case "chrome":
386450
- return ["chrome", "chromedriver"];
386451
- case "firefox":
386452
- return ["firefox", "geckodriver"];
386453
- default:
386454
- return [];
386455
- }
386456
- }
386457
- async function loadPuppeteerBrowsers(deps, ctx) {
386458
- if (deps.browsersModule)
386459
- return deps.browsersModule;
386460
- return await loadHeavyDep("@puppeteer/browsers", { ctx, deps: { logger: deps.logger } });
386461
- }
386462
- async function loadGeckodriver(deps, ctx) {
386463
- if (deps.geckodriverModule)
386464
- return deps.geckodriverModule;
386465
- return await loadHeavyDep("geckodriver", { ctx, deps: { logger: deps.logger } });
386466
- }
386467
- function isStillFresh(latestCheckedAt, now) {
386468
- if (!latestCheckedAt)
386469
- return false;
386470
- const t = Date.parse(latestCheckedAt);
386471
- if (Number.isNaN(t))
386472
- return false;
386473
- return now.getTime() - t < FRESHNESS_TTL_MS;
386474
- }
386475
- async function resolveChannelBuildId(browsersModule, assetName, platform) {
386476
- const channel = BROWSER_CHANNELS[assetName];
386477
- return await withTimeout(Promise.resolve(browsersModule.resolveBuildId(assetName, platform, channel)), RESOLVE_TIMEOUT_MS, `resolveBuildId(${assetName}, ${channel})`);
386478
- }
386479
- function withTimeout(p, ms, label) {
386480
- return new Promise((resolve, reject) => {
386481
- const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
386482
- if (typeof t.unref === "function")
386483
- t.unref();
386484
- p.then((v) => {
386485
- clearTimeout(t);
386486
- resolve(v);
386487
- }, (err) => {
386488
- clearTimeout(t);
386489
- reject(err);
386490
- });
386491
- });
386492
- }
386493
- async function ensureBrowserInstalled(name, options = {}) {
386494
- const { ctx = {} } = options;
386495
- const dedupeKey = `${name}:${getBrowsersDir(ctx)}`;
386496
- const inFlight = inFlightInstalls.get(dedupeKey);
386497
- if (inFlight)
386498
- return inFlight;
386499
- const p = ensureBrowserInstalledImpl(name, options).finally(() => {
386500
- inFlightInstalls.delete(dedupeKey);
386501
- });
386502
- inFlightInstalls.set(dedupeKey, p);
386503
- return p;
386504
- }
386505
- async function ensureBrowserInstalledImpl(name, options = {}) {
386506
- const { ctx = {}, deps = {}, force = false } = options;
386507
- const logger = deps.logger ?? defaultLogger2;
386508
- const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
386509
- const cacheDir = getBrowsersDir(ctx);
386510
- const record = readInstalledRecord(ctx);
386511
- const existing = record.browsers[name];
386512
- if (name === "geckodriver") {
386513
- return await ensureGeckodriver({ ctx, deps: { ...deps, logger }, force, now }, record, existing, logger);
386514
- }
386515
- const browsersModule = await loadPuppeteerBrowsers(deps, ctx);
386516
- const platform = browsersModule.detectBrowserPlatform ? browsersModule.detectBrowserPlatform() : void 0;
386517
- if (!platform) {
386518
- throw new Error(`Unable to determine browser platform on ${process.platform}/${process.arch}. @puppeteer/browsers does not recognize this OS; ${name} cannot be installed automatically. Set DOC_DETECTIVE_CACHE_DIR to a pre-warmed cache or skip browser-driven steps.`);
386519
- }
386520
- if (!force && existing && isStillFresh(existing.latestCheckedAt, now)) {
386521
- if (existing.latestKnownVersion === existing.installedVersion) {
386522
- const path23 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
386523
- return { path: path23, version: existing.installedVersion, outdated: false };
386524
- }
386525
- const path22 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
386526
- 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");
386527
- return { path: path22, version: existing.installedVersion, outdated: true };
386528
- }
386529
- let latest;
386530
- try {
386531
- latest = await resolveChannelBuildId(browsersModule, name, platform);
386532
- } catch (err) {
386533
- logger(`Channel resolution for ${name} skipped: ${String(err)}`, "debug");
386534
- if (existing) {
386535
- const path22 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
386536
- return { path: path22, version: existing.installedVersion, outdated: false };
386537
- }
386538
- throw err;
386539
- }
386540
- if (!force && existing && existing.installedVersion === latest) {
386541
- record.browsers[name] = {
386542
- ...existing,
386543
- latestKnownVersion: latest,
386544
- latestCheckedAt: now.toISOString()
386545
- };
386546
- writeInstalledRecord(record, ctx);
386547
- const path22 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
386548
- return { path: path22, version: latest, outdated: false };
386549
- }
386550
- if (!force && existing) {
386551
- record.browsers[name] = {
386552
- ...existing,
386553
- latestKnownVersion: latest,
386554
- latestCheckedAt: now.toISOString()
386555
- };
386556
- writeInstalledRecord(record, ctx);
386557
- logger(`${name} ${existing.installedVersion} installed in ${cacheDir}; "${BROWSER_CHANNELS[name]}" channel is now ${latest}. Run \`doc-detective install browsers ${name} --force\` to update.`, "warn");
386558
- const path22 = await locateExecutable(browsersModule, name, existing.installedVersion, cacheDir, platform);
386559
- return { path: path22, version: existing.installedVersion, outdated: true };
386560
- }
386561
- logger(`Installing ${name} ${latest} into ${cacheDir}`, "info");
386562
- await browsersModule.install({
386563
- browser: name,
386564
- buildId: latest,
386565
- cacheDir
386566
- });
386567
- if (existing && existing.installedVersion !== latest) {
386568
- try {
386569
- await browsersModule.uninstall({
386570
- browser: name,
386571
- buildId: existing.installedVersion,
386572
- cacheDir
386573
- });
386574
- } catch (err) {
386575
- logger(`Failed to prune old ${name} buildId ${existing.installedVersion}: ${String(err)}`, "debug");
386576
- }
386577
- }
386578
- record.browsers[name] = {
386579
- installedVersion: latest,
386580
- installedAt: now.toISOString(),
386581
- latestKnownVersion: latest,
386582
- latestCheckedAt: now.toISOString()
386583
- };
386584
- writeInstalledRecord(record, ctx);
386585
- const path21 = await locateExecutable(browsersModule, name, latest, cacheDir, platform);
386586
- return { path: path21, version: latest, outdated: false };
386587
- }
386588
- async function locateExecutable(browsersModule, name, buildId, cacheDir, platform) {
386589
- if (typeof browsersModule.computeExecutablePath === "function") {
386590
- return browsersModule.computeExecutablePath({
386591
- browser: name,
386592
- buildId,
386593
- cacheDir,
386594
- platform
386595
- });
386596
- }
386597
- return cacheDir;
386598
- }
386599
- async function ensureGeckodriver(ctxBag, record, existing, logger) {
386600
- const cacheDir = getBrowsersDir(ctxBag.ctx);
386601
- const resolveBinaryPath = (gecko2) => {
386602
- const fromModule = gecko2 && typeof gecko2.path === "string" && gecko2.path.length > 0 ? gecko2.path : null;
386603
- return fromModule ?? cacheDir;
386604
- };
386605
- if (!ctxBag.force && existing && isStillFresh(existing.latestCheckedAt, ctxBag.now)) {
386606
- const gecko2 = ctxBag.deps.geckodriverModule ?? await loadGeckodriver(ctxBag.deps, ctxBag.ctx);
386607
- return {
386608
- path: resolveBinaryPath(gecko2),
386609
- version: existing.installedVersion,
386610
- // Only mark outdated when we actually KNOW a different latest
386611
- // version exists. If `latestKnownVersion` is undefined (e.g., the
386612
- // installer couldn't determine a version and we recorded
386613
- // `installedVersion: "unknown"` with `latestKnownVersion: undefined`),
386614
- // a naive `!==` comparison would flag the entry as outdated on
386615
- // every run.
386616
- outdated: typeof existing.latestKnownVersion === "string" && existing.latestKnownVersion.length > 0 && existing.latestKnownVersion !== existing.installedVersion
386617
- };
386618
- }
386619
- const gecko = await loadGeckodriver(ctxBag.deps, ctxBag.ctx);
386620
- const prevEnv = process.env.GECKODRIVER_CACHE_DIR;
386621
- process.env.GECKODRIVER_CACHE_DIR = cacheDir;
386622
- try {
386623
- logger(`Installing geckodriver into ${cacheDir}`, "info");
386624
- const downloadResult = await gecko.download();
386625
- const resolvedVersion = (typeof gecko.GECKODRIVER_VERSION === "string" ? gecko.GECKODRIVER_VERSION : void 0) || (downloadResult && typeof downloadResult.version === "string" ? downloadResult.version : void 0);
386626
- if (!resolvedVersion) {
386627
- logger("Could not determine geckodriver version after install. Freshness checks for geckodriver will be unreliable until the next forced reinstall.", "warn");
386628
- }
386629
- const installedVersion = resolvedVersion ?? "unknown";
386630
- record.browsers.geckodriver = {
386631
- installedVersion,
386632
- installedAt: ctxBag.now.toISOString(),
386633
- latestKnownVersion: resolvedVersion,
386634
- latestCheckedAt: ctxBag.now.toISOString()
386635
- };
386636
- writeInstalledRecord(record, ctxBag.ctx);
386637
- return {
386638
- path: resolveBinaryPath(gecko),
386639
- version: installedVersion,
386640
- outdated: false
386641
- };
386642
- } finally {
386643
- if (prevEnv === void 0)
386644
- delete process.env.GECKODRIVER_CACHE_DIR;
386645
- else
386646
- process.env.GECKODRIVER_CACHE_DIR = prevEnv;
386647
- }
386648
- }
386649
- function getInstalledBrowsers(ctx = {}) {
386650
- const record = readInstalledRecord(ctx);
386651
- return Object.entries(record.browsers).map(([name, entry]) => ({
386652
- name,
386653
- installedVersion: entry.installedVersion,
386654
- installedAt: entry.installedAt,
386655
- latestKnownVersion: entry.latestKnownVersion
386656
- }));
386657
- }
386658
- var BROWSER_CHANNELS, FRESHNESS_TTL_MS, RESOLVE_TIMEOUT_MS, RUNTIME_DEBUG2, defaultLogger2, inFlightInstalls;
386659
- var init_browsers = __esm({
386660
- "dist/runtime/browsers.js"() {
386661
- "use strict";
386662
- init_cacheDir();
386663
- init_loader();
386664
- BROWSER_CHANNELS = {
386665
- chrome: "stable",
386666
- firefox: "latest",
386667
- chromedriver: "stable",
386668
- geckodriver: "latest"
386669
- };
386670
- FRESHNESS_TTL_MS = 24 * 60 * 60 * 1e3;
386671
- RESOLVE_TIMEOUT_MS = 5e3;
386672
- RUNTIME_DEBUG2 = process.env.DOC_DETECTIVE_RUNTIME_DEBUG === "1";
386673
- defaultLogger2 = (msg, level = "info") => {
386674
- if (level === "debug" && !RUNTIME_DEBUG2)
386675
- return;
386676
- if (level === "error")
386677
- console.error(msg);
386678
- else
386679
- console.log(msg);
386680
- };
386681
- inFlightInstalls = /* @__PURE__ */ new Map();
386682
- }
386683
- });
386684
-
386685
387156
  // dist/runtime/inferRuntimeNeeds.js
386686
387157
  var inferRuntimeNeeds_exports = {};
386687
387158
  __export(inferRuntimeNeeds_exports, {
@@ -386819,8 +387290,8 @@ module.exports = __toCommonJS(index_exports);
386819
387290
  init_config();
386820
387291
 
386821
387292
  // dist/core/detectTests.js
386822
- var import_node_fs9 = __toESM(require("node:fs"), 1);
386823
- var import_node_path9 = __toESM(require("node:path"), 1);
387293
+ var import_node_fs10 = __toESM(require("node:fs"), 1);
387294
+ var import_node_path10 = __toESM(require("node:path"), 1);
386824
387295
  var import_node_os5 = __toESM(require("node:os"), 1);
386825
387296
  var import_node_crypto3 = __toESM(require("node:crypto"), 1);
386826
387297
  var import_yaml3 = __toESM(require("yaml"), 1);
@@ -387693,8 +388164,8 @@ init_files();
387693
388164
  init_utils();
387694
388165
 
387695
388166
  // dist/core/integrations/heretto.js
387696
- var import_node_fs8 = __toESM(require("node:fs"), 1);
387697
- var import_node_path8 = __toESM(require("node:path"), 1);
388167
+ var import_node_fs9 = __toESM(require("node:fs"), 1);
388168
+ var import_node_path9 = __toESM(require("node:path"), 1);
387698
388169
  var import_node_https = __toESM(require("node:https"), 1);
387699
388170
  var import_node_http = __toESM(require("node:http"), 1);
387700
388171
  var import_axios3 = __toESM(require("axios"), 1);
@@ -387736,7 +388207,7 @@ var HerettoUploader = class {
387736
388207
  const apiBaseUrl = `https://${integrationConfig.organizationId}.heretto.com`;
387737
388208
  let fileId = sourceIntegration.fileId;
387738
388209
  let parentFolderId = sourceIntegration.parentFolderId;
387739
- const filename = import_node_path8.default.basename(sourceIntegration.filePath);
388210
+ const filename = import_node_path9.default.basename(sourceIntegration.filePath);
387740
388211
  const relativeFilePath = sourceIntegration.filePath;
387741
388212
  if (!fileId && integrationConfig.resourceDependencies) {
387742
388213
  const resolvedFile = this.resolveFromDependencies({
@@ -387776,9 +388247,9 @@ var HerettoUploader = class {
387776
388247
  }
387777
388248
  }
387778
388249
  if (!parentFolderId && relativeFilePath) {
387779
- const parentDirPath = import_node_path8.default.dirname(relativeFilePath);
388250
+ const parentDirPath = import_node_path9.default.dirname(relativeFilePath);
387780
388251
  if (parentDirPath && parentDirPath !== ".") {
387781
- const folderName = import_node_path8.default.basename(parentDirPath);
388252
+ const folderName = import_node_path9.default.basename(parentDirPath);
387782
388253
  log3(config, "debug", `Searching for parent folder by name: ${folderName}`);
387783
388254
  parentFolderId = await this.searchFolderByName({
387784
388255
  apiBaseUrl,
@@ -387854,11 +388325,11 @@ var HerettoUploader = class {
387854
388325
  return result;
387855
388326
  }
387856
388327
  }
387857
- if (!import_node_fs8.default.existsSync(localFilePath)) {
388328
+ if (!import_node_fs9.default.existsSync(localFilePath)) {
387858
388329
  result.description = `Local file not found: ${localFilePath}`;
387859
388330
  return result;
387860
388331
  }
387861
- const fileContent = import_node_fs8.default.readFileSync(localFilePath);
388332
+ const fileContent = import_node_fs9.default.readFileSync(localFilePath);
387862
388333
  const contentType = this.getContentType(localFilePath);
387863
388334
  try {
387864
388335
  await this.uploadFile({
@@ -387889,7 +388360,7 @@ var HerettoUploader = class {
387889
388360
  resolveFromDependencies({ resourceDependencies, filePath, filename, log: log3 }) {
387890
388361
  if (!resourceDependencies)
387891
388362
  return null;
387892
- const normalizedPath = import_node_path8.default.posix.normalize(filePath.replace(/\\/g, "/")).replace(/^\.\.\/+/g, "").replace(/^\.\//, "");
388363
+ const normalizedPath = import_node_path9.default.posix.normalize(filePath.replace(/\\/g, "/")).replace(/^\.\.\/+/g, "").replace(/^\.\//, "");
387893
388364
  for (const [depPath, info] of Object.entries(resourceDependencies)) {
387894
388365
  if (depPath.startsWith("_"))
387895
388366
  continue;
@@ -387899,14 +388370,14 @@ var HerettoUploader = class {
387899
388370
  return info;
387900
388371
  }
387901
388372
  }
387902
- const parentDir = import_node_path8.default.dirname(normalizedPath);
387903
- const parentFolderName = import_node_path8.default.basename(parentDir);
388373
+ const parentDir = import_node_path9.default.dirname(normalizedPath);
388374
+ const parentFolderName = import_node_path9.default.basename(parentDir);
387904
388375
  for (const [depPath, info] of Object.entries(resourceDependencies)) {
387905
388376
  if (depPath.startsWith("_"))
387906
388377
  continue;
387907
- const depFilename = import_node_path8.default.basename(depPath);
387908
- const depParentDir = import_node_path8.default.dirname(depPath);
387909
- const depParentFolderName = import_node_path8.default.basename(depParentDir);
388378
+ const depFilename = import_node_path9.default.basename(depPath);
388379
+ const depParentDir = import_node_path9.default.dirname(depPath);
388380
+ const depParentFolderName = import_node_path9.default.basename(depParentDir);
387910
388381
  if (depFilename === filename && depParentFolderName === parentFolderName) {
387911
388382
  log3("debug", `Found filename+folder match in dependencies: ${depPath}`);
387912
388383
  return info;
@@ -387915,7 +388386,7 @@ var HerettoUploader = class {
387915
388386
  for (const [depPath, info] of Object.entries(resourceDependencies)) {
387916
388387
  if (depPath.startsWith("_"))
387917
388388
  continue;
387918
- const depFilename = import_node_path8.default.basename(depPath);
388389
+ const depFilename = import_node_path9.default.basename(depPath);
387919
388390
  if (depFilename === filename) {
387920
388391
  log3("debug", `Found filename match in dependencies: ${depPath}`);
387921
388392
  return info;
@@ -387946,17 +388417,17 @@ var HerettoUploader = class {
387946
388417
  };
387947
388418
  if (!resourceDependencies)
387948
388419
  return result;
387949
- const normalizedPath = import_node_path8.default.posix.normalize(filePath.replace(/\\/g, "/")).replace(/^(\.\.\/)+/g, "").replace(/^\.\//, "");
387950
- const parentDir = import_node_path8.default.dirname(normalizedPath);
387951
- const targetFolderName = import_node_path8.default.basename(parentDir);
388420
+ const normalizedPath = import_node_path9.default.posix.normalize(filePath.replace(/\\/g, "/")).replace(/^(\.\.\/)+/g, "").replace(/^\.\//, "");
388421
+ const parentDir = import_node_path9.default.dirname(normalizedPath);
388422
+ const targetFolderName = import_node_path9.default.basename(parentDir);
387952
388423
  result.targetFolderName = targetFolderName;
387953
388424
  result.ditamapParentFolderId = resourceDependencies._ditamapParentFolderId || null;
387954
388425
  log3("debug", `Looking for parent folder '${targetFolderName}' in dependencies`);
387955
388426
  for (const [depPath, info] of Object.entries(resourceDependencies)) {
387956
388427
  if (depPath.startsWith("_"))
387957
388428
  continue;
387958
- const depParentDir = import_node_path8.default.dirname(depPath);
387959
- const depFolderName = import_node_path8.default.basename(depParentDir);
388429
+ const depParentDir = import_node_path9.default.dirname(depPath);
388430
+ const depFolderName = import_node_path9.default.basename(depParentDir);
387960
388431
  if (depFolderName === targetFolderName && info.parentFolderId) {
387961
388432
  log3("debug", `Found sibling file ${depPath} with parent folder ID: ${info.parentFolderId}`);
387962
388433
  result.folderId = info.parentFolderId;
@@ -388460,7 +388931,7 @@ var HerettoUploader = class {
388460
388931
  * @returns {string} MIME content type
388461
388932
  */
388462
388933
  getContentType(filePath) {
388463
- const ext = import_node_path8.default.extname(filePath).toLowerCase();
388934
+ const ext = import_node_path9.default.extname(filePath).toLowerCase();
388464
388935
  const contentTypes = {
388465
388936
  ".png": "image/png",
388466
388937
  ".jpg": "image/jpeg",
@@ -388632,39 +389103,39 @@ async function pollJobStatus(client, fileId, jobId, log3, config) {
388632
389103
  return null;
388633
389104
  }
388634
389105
  async function downloadAndExtractOutput(client, fileId, jobId, herettoName, log3, config, deps) {
388635
- const fsModule = deps?.fsModule || import_node_fs8.default;
389106
+ const fsModule = deps?.fsModule || import_node_fs9.default;
388636
389107
  const ZipClass = deps?.ZipClass || import_adm_zip.default;
388637
- const tempDir = import_node_path8.default.join(import_node_os4.default.tmpdir(), "doc-detective");
389108
+ const tempDir = import_node_path9.default.join(import_node_os4.default.tmpdir(), "doc-detective");
388638
389109
  const hash = import_node_crypto2.default.createHash("md5").update(`${herettoName}_${jobId}`).digest("hex");
388639
389110
  try {
388640
389111
  fsModule.mkdirSync(tempDir, { recursive: true });
388641
- const outputDir = import_node_path8.default.join(tempDir, `heretto_${hash}`);
389112
+ const outputDir = import_node_path9.default.join(tempDir, `heretto_${hash}`);
388642
389113
  log3(config, "debug", `Downloading publishing job output for ${herettoName}...`);
388643
389114
  const response = await client.get(`/files/${fileId}/publishes/${jobId}/assets-all`, {
388644
389115
  responseType: "arraybuffer",
388645
389116
  timeout: DOWNLOAD_TIMEOUT_MS,
388646
389117
  headers: { Accept: "application/octet-stream" }
388647
389118
  });
388648
- const zipPath = import_node_path8.default.join(tempDir, `heretto_${hash}.zip`);
389119
+ const zipPath = import_node_path9.default.join(tempDir, `heretto_${hash}.zip`);
388649
389120
  fsModule.writeFileSync(zipPath, response.data);
388650
389121
  log3(config, "debug", `Extracting output to ${outputDir}...`);
388651
389122
  const zip = new ZipClass(zipPath);
388652
- const resolvedOutputDir = import_node_path8.default.resolve(outputDir);
389123
+ const resolvedOutputDir = import_node_path9.default.resolve(outputDir);
388653
389124
  for (const entry of zip.getEntries()) {
388654
- const normalizedName = import_node_path8.default.posix.normalize(entry.entryName.replace(/\\/g, "/"));
389125
+ const normalizedName = import_node_path9.default.posix.normalize(entry.entryName.replace(/\\/g, "/"));
388655
389126
  if (normalizedName.startsWith("..") || normalizedName.includes("/..")) {
388656
389127
  log3(config, "warning", `Skipping ZIP entry with path traversal: ${entry.entryName}`);
388657
389128
  continue;
388658
389129
  }
388659
- const resolvedPath = import_node_path8.default.resolve(outputDir, normalizedName);
388660
- if (!resolvedPath.startsWith(resolvedOutputDir + import_node_path8.default.sep) && resolvedPath !== resolvedOutputDir) {
389130
+ const resolvedPath = import_node_path9.default.resolve(outputDir, normalizedName);
389131
+ if (!resolvedPath.startsWith(resolvedOutputDir + import_node_path9.default.sep) && resolvedPath !== resolvedOutputDir) {
388661
389132
  log3(config, "warning", `Skipping ZIP entry outside output directory: ${entry.entryName}`);
388662
389133
  continue;
388663
389134
  }
388664
389135
  if (entry.isDirectory) {
388665
389136
  fsModule.mkdirSync(resolvedPath, { recursive: true });
388666
389137
  } else {
388667
- fsModule.mkdirSync(import_node_path8.default.dirname(resolvedPath), { recursive: true });
389138
+ fsModule.mkdirSync(import_node_path9.default.dirname(resolvedPath), { recursive: true });
388668
389139
  fsModule.writeFileSync(resolvedPath, entry.getData());
388669
389140
  }
388670
389141
  }
@@ -388673,14 +389144,14 @@ async function downloadAndExtractOutput(client, fileId, jobId, herettoName, log3
388673
389144
  return outputDir;
388674
389145
  } catch (error) {
388675
389146
  try {
388676
- const zipCleanupPath = import_node_path8.default.join(tempDir, `heretto_${hash}.zip`);
389147
+ const zipCleanupPath = import_node_path9.default.join(tempDir, `heretto_${hash}.zip`);
388677
389148
  if (fsModule.existsSync(zipCleanupPath)) {
388678
389149
  fsModule.unlinkSync(zipCleanupPath);
388679
389150
  }
388680
389151
  } catch {
388681
389152
  }
388682
389153
  try {
388683
- const cleanupOutputDir = import_node_path8.default.join(tempDir, `heretto_${hash}`);
389154
+ const cleanupOutputDir = import_node_path9.default.join(tempDir, `heretto_${hash}`);
388684
389155
  if (fsModule.existsSync(cleanupOutputDir)) {
388685
389156
  fsModule.rmSync(cleanupOutputDir, { recursive: true, force: true });
388686
389157
  }
@@ -388759,7 +389230,7 @@ async function getResourceDependencies(restClient, ditamapId, log3, config) {
388759
389230
  pathToUuidMap[relativePath] = {
388760
389231
  uuid,
388761
389232
  fullPath: uri,
388762
- name: name || import_node_path8.default.basename(relativePath || ""),
389233
+ name: name || import_node_path9.default.basename(relativePath || ""),
388763
389234
  parentFolderId
388764
389235
  };
388765
389236
  }
@@ -388827,12 +389298,12 @@ async function detectTests2({ config }) {
388827
389298
  return specs;
388828
389299
  }
388829
389300
  function generateSpecId(filePath) {
388830
- const absolutePath = import_node_path9.default.resolve(filePath);
389301
+ const absolutePath = import_node_path10.default.resolve(filePath);
388831
389302
  const cwd = process.cwd();
388832
- const candidate = import_node_path9.default.relative(cwd, absolutePath);
388833
- const isInsideCwd = candidate.length > 0 && !candidate.startsWith("..") && !import_node_path9.default.isAbsolute(candidate);
389303
+ const candidate = import_node_path10.default.relative(cwd, absolutePath);
389304
+ const isInsideCwd = candidate.length > 0 && !candidate.startsWith("..") && !import_node_path10.default.isAbsolute(candidate);
388834
389305
  const relativePath = isInsideCwd ? candidate : absolutePath;
388835
- const normalizedPath = relativePath.split(import_node_path9.default.sep).join("/").replace(/^\.\//, "").replace(/[^a-zA-Z0-9._\-\/]/g, "_");
389306
+ const normalizedPath = relativePath.split(import_node_path10.default.sep).join("/").replace(/^\.\//, "").replace(/[^a-zA-Z0-9._\-\/]/g, "_");
388836
389307
  return normalizedPath;
388837
389308
  }
388838
389309
  async function isValidSourceFile({ config, files, source }) {
@@ -388843,7 +389314,7 @@ async function isValidSourceFile({ config, files, source }) {
388843
389314
  });
388844
389315
  if (files.indexOf(source) >= 0)
388845
389316
  return false;
388846
- if (import_node_path9.default.extname(source) === ".json" || import_node_path9.default.extname(source) === ".yaml" || import_node_path9.default.extname(source) === ".yml") {
389317
+ if (import_node_path10.default.extname(source) === ".json" || import_node_path10.default.extname(source) === ".yaml" || import_node_path10.default.extname(source) === ".yml") {
388847
389318
  const content = await readFile({ fileURLOrPath: source });
388848
389319
  if (typeof content !== "object") {
388849
389320
  log(config, "debug", `${source} isn't a valid test specification. Skipping.`);
@@ -388863,11 +389334,11 @@ async function isValidSourceFile({ config, files, source }) {
388863
389334
  if (test.before) {
388864
389335
  let beforePath = "";
388865
389336
  if (config.relativePathBase === "file") {
388866
- beforePath = import_node_path9.default.resolve(import_node_path9.default.dirname(source), test.before);
389337
+ beforePath = import_node_path10.default.resolve(import_node_path10.default.dirname(source), test.before);
388867
389338
  } else {
388868
- beforePath = import_node_path9.default.resolve(test.before);
389339
+ beforePath = import_node_path10.default.resolve(test.before);
388869
389340
  }
388870
- if (!import_node_fs9.default.existsSync(beforePath)) {
389341
+ if (!import_node_fs10.default.existsSync(beforePath)) {
388871
389342
  log(config, "debug", `${beforePath} is specified to run before a test but isn't a valid file. Skipping ${source}.`);
388872
389343
  return false;
388873
389344
  }
@@ -388875,18 +389346,18 @@ async function isValidSourceFile({ config, files, source }) {
388875
389346
  if (test.after) {
388876
389347
  let afterPath = "";
388877
389348
  if (config.relativePathBase === "file") {
388878
- afterPath = import_node_path9.default.resolve(import_node_path9.default.dirname(source), test.after);
389349
+ afterPath = import_node_path10.default.resolve(import_node_path10.default.dirname(source), test.after);
388879
389350
  } else {
388880
- afterPath = import_node_path9.default.resolve(test.after);
389351
+ afterPath = import_node_path10.default.resolve(test.after);
388881
389352
  }
388882
- if (!import_node_fs9.default.existsSync(afterPath)) {
389353
+ if (!import_node_fs10.default.existsSync(afterPath)) {
388883
389354
  log(config, "debug", `${afterPath} is specified to run after a test but isn't a valid file. Skipping ${source}.`);
388884
389355
  return false;
388885
389356
  }
388886
389357
  }
388887
389358
  }
388888
389359
  }
388889
- const extension = import_node_path9.default.extname(source).substring(1);
389360
+ const extension = import_node_path10.default.extname(source).substring(1);
388890
389361
  if (!allowedExtensions.includes(extension)) {
388891
389362
  log(config, "debug", `${source} extension isn't specified in a \`config.fileTypes\` object. Skipping.`);
388892
389363
  return false;
@@ -388895,11 +389366,11 @@ async function isValidSourceFile({ config, files, source }) {
388895
389366
  }
388896
389367
  async function processDitaMap({ config, source }) {
388897
389368
  const hash = import_node_crypto3.default.createHash("md5").update(source).digest("hex");
388898
- const tmpBase = import_node_path9.default.join(import_node_os5.default.tmpdir(), "doc-detective");
388899
- const outputDir = import_node_path9.default.join(tmpBase, `ditamap_${hash}`);
388900
- if (!import_node_fs9.default.existsSync(tmpBase)) {
389369
+ const tmpBase = import_node_path10.default.join(import_node_os5.default.tmpdir(), "doc-detective");
389370
+ const outputDir = import_node_path10.default.join(tmpBase, `ditamap_${hash}`);
389371
+ if (!import_node_fs10.default.existsSync(tmpBase)) {
388901
389372
  log(config, "debug", `Creating temp directory: ${tmpBase}`);
388902
- import_node_fs9.default.mkdirSync(tmpBase, { recursive: true });
389373
+ import_node_fs10.default.mkdirSync(tmpBase, { recursive: true });
388903
389374
  }
388904
389375
  const ditaVersion = await spawnCommand("dita", ["--version"]);
388905
389376
  if (ditaVersion.exitCode !== 0) {
@@ -389006,13 +389477,13 @@ async function qualifyFiles({ config }) {
389006
389477
  let isFile = false;
389007
389478
  let isDir = false;
389008
389479
  try {
389009
- isFile = import_node_fs9.default.statSync(source).isFile();
389010
- isDir = import_node_fs9.default.statSync(source).isDirectory();
389480
+ isFile = import_node_fs10.default.statSync(source).isFile();
389481
+ isDir = import_node_fs10.default.statSync(source).isDirectory();
389011
389482
  } catch {
389012
389483
  log(config, "warning", `Cannot access path: ${source}. Skipping.`);
389013
389484
  continue;
389014
389485
  }
389015
- if (isFile && import_node_path9.default.extname(source) === ".ditamap" && !ignoredDitaMaps.some((ignored) => source.includes(ignored)) && config.processDitaMaps) {
389486
+ if (isFile && import_node_path10.default.extname(source) === ".ditamap" && !ignoredDitaMaps.some((ignored) => source.includes(ignored)) && config.processDitaMaps) {
389016
389487
  const ditaOutput = await processDitaMap({ config, source });
389017
389488
  if (ditaOutput) {
389018
389489
  const currentIndex = sequence.findIndex((e) => e.source === source);
@@ -389021,7 +389492,7 @@ async function qualifyFiles({ config }) {
389021
389492
  }
389022
389493
  continue;
389023
389494
  }
389024
- const resolved = import_node_path9.default.resolve(source);
389495
+ const resolved = import_node_path10.default.resolve(source);
389025
389496
  if (isFile && await isValidSourceFile({ config, files, source: resolved })) {
389026
389497
  files.push(resolved);
389027
389498
  if (!phaseByFile.has(resolved))
@@ -389030,15 +389501,15 @@ async function qualifyFiles({ config }) {
389030
389501
  dirs = [];
389031
389502
  dirs[0] = source;
389032
389503
  for (const dir of dirs) {
389033
- const objects = import_node_fs9.default.readdirSync(dir);
389504
+ const objects = import_node_fs10.default.readdirSync(dir);
389034
389505
  for (const object of objects) {
389035
- const content = import_node_path9.default.resolve(dir + "/" + object);
389506
+ const content = import_node_path10.default.resolve(dir + "/" + object);
389036
389507
  if (content.includes("node_modules"))
389037
389508
  continue;
389038
- const isFile2 = import_node_fs9.default.statSync(content).isFile();
389039
- const isDir2 = import_node_fs9.default.statSync(content).isDirectory();
389509
+ const isFile2 = import_node_fs10.default.statSync(content).isFile();
389510
+ const isDir2 = import_node_fs10.default.statSync(content).isDirectory();
389040
389511
  if (isFile2 && await isValidSourceFile({ config, files, source: content })) {
389041
- const resolved2 = import_node_path9.default.resolve(content);
389512
+ const resolved2 = import_node_path10.default.resolve(content);
389042
389513
  files.push(resolved2);
389043
389514
  if (!phaseByFile.has(resolved2))
389044
389515
  phaseByFile.set(resolved2, phase);
@@ -389055,12 +389526,12 @@ async function parseTests({ config, files }) {
389055
389526
  let specs = [];
389056
389527
  for (const file of files) {
389057
389528
  log(config, "debug", `file: ${file}`);
389058
- const extension = import_node_path9.default.extname(file).slice(1);
389529
+ const extension = import_node_path10.default.extname(file).slice(1);
389059
389530
  let content = "";
389060
389531
  let rawContent;
389061
389532
  if (extension === "json" || extension === "yaml" || extension === "yml") {
389062
389533
  try {
389063
- rawContent = await import_node_fs9.default.promises.readFile(file, "utf8");
389534
+ rawContent = await import_node_fs10.default.promises.readFile(file, "utf8");
389064
389535
  if (extension === "json") {
389065
389536
  content = JSON.parse(rawContent);
389066
389537
  } else {
@@ -389078,7 +389549,7 @@ async function parseTests({ config, files }) {
389078
389549
  content.specId = generateSpecId(file);
389079
389550
  if (!content.contentPath)
389080
389551
  content.contentPath = file;
389081
- const specPhase = config._phaseByFile?.get(import_node_path9.default.resolve(file)) ?? "main";
389552
+ const specPhase = config._phaseByFile?.get(import_node_path10.default.resolve(file)) ?? "main";
389082
389553
  content._phase = specPhase;
389083
389554
  if (Array.isArray(content.tests)) {
389084
389555
  const usedTestIds = new Set(content.tests.map((test) => test?.testId).filter((id) => id));
@@ -389246,7 +389717,7 @@ async function parseTests({ config, files }) {
389246
389717
  let spec = {
389247
389718
  specId: id,
389248
389719
  contentPath: file,
389249
- _phase: config._phaseByFile?.get(import_node_path9.default.resolve(file)) ?? "main",
389720
+ _phase: config._phaseByFile?.get(import_node_path10.default.resolve(file)) ?? "main",
389250
389721
  tests: []
389251
389722
  };
389252
389723
  const fileType = config.fileTypes.find((fileType2) => fileType2.extensions.includes(extension));
@@ -389295,7 +389766,7 @@ async function parseTests({ config, files }) {
389295
389766
  object: spec,
389296
389767
  filePath: file
389297
389768
  });
389298
- spec._phase = config._phaseByFile?.get(import_node_path9.default.resolve(file)) ?? "main";
389769
+ spec._phase = config._phaseByFile?.get(import_node_path10.default.resolve(file)) ?? "main";
389299
389770
  specs.push(spec);
389300
389771
  }
389301
389772
  }
@@ -389329,6 +389800,7 @@ function resolveContexts({ contexts, test, config }) {
389329
389800
  }
389330
389801
  if (browser.name === "safari")
389331
389802
  browser.name = "webkit";
389803
+ browser.explicit = true;
389332
389804
  return browser;
389333
389805
  });
389334
389806
  }
@@ -389508,7 +389980,7 @@ init_utils();
389508
389980
 
389509
389981
  // dist/core/tests.js
389510
389982
  var import_tree_kill3 = __toESM(require("tree-kill"), 1);
389511
- var import_node_fs21 = __toESM(require("node:fs"), 1);
389983
+ var import_node_fs22 = __toESM(require("node:fs"), 1);
389512
389984
  init_loader();
389513
389985
  init_browsers();
389514
389986
  init_browserStepKeys();
@@ -390065,10 +390537,10 @@ init_validate();
390065
390537
  init_loader();
390066
390538
 
390067
390539
  // dist/core/tests/ffmpegRecorder.js
390068
- var import_node_child_process4 = require("node:child_process");
390540
+ var import_node_child_process5 = require("node:child_process");
390069
390541
  var import_node_os6 = __toESM(require("node:os"), 1);
390070
- var import_node_path10 = __toESM(require("node:path"), 1);
390071
- var import_node_fs10 = __toESM(require("node:fs"), 1);
390542
+ var import_node_path11 = __toESM(require("node:path"), 1);
390543
+ var import_node_fs11 = __toESM(require("node:fs"), 1);
390072
390544
  var import_node_crypto5 = __toESM(require("node:crypto"), 1);
390073
390545
  init_loader();
390074
390546
  init_utils();
@@ -390147,7 +390619,7 @@ function browserCaptureTitle(contextId) {
390147
390619
  return `RECORD_ME_${safeContextId(contextId)}`;
390148
390620
  }
390149
390621
  function browserDownloadDir(contextId) {
390150
- return import_node_path10.default.join(import_node_os6.default.tmpdir(), "doc-detective", "recordings", safeContextId(contextId));
390622
+ return import_node_path11.default.join(import_node_os6.default.tmpdir(), "doc-detective", "recordings", safeContextId(contextId));
390151
390623
  }
390152
390624
  function engineFields(record) {
390153
390625
  const engine = record && typeof record === "object" ? record.engine : void 0;
@@ -390384,7 +390856,7 @@ async function detectMacScreenIndex(ffmpegPath) {
390384
390856
  resolve(v);
390385
390857
  };
390386
390858
  try {
390387
- proc = (0, import_node_child_process4.spawn)(ffmpegPath, ["-f", "avfoundation", "-list_devices", "true", "-i", ""], { stdio: ["ignore", "ignore", "pipe"] });
390859
+ proc = (0, import_node_child_process5.spawn)(ffmpegPath, ["-f", "avfoundation", "-list_devices", "true", "-i", ""], { stdio: ["ignore", "ignore", "pipe"] });
390388
390860
  proc.stderr?.on("data", (d) => {
390389
390861
  out += d.toString();
390390
390862
  });
@@ -390413,7 +390885,7 @@ async function detectX11ScreenSize(display) {
390413
390885
  };
390414
390886
  try {
390415
390887
  const env = display ? { ...process.env, DISPLAY: display } : process.env;
390416
- proc = (0, import_node_child_process4.spawn)("xdpyinfo", [], { env, stdio: ["ignore", "pipe", "ignore"] });
390888
+ proc = (0, import_node_child_process5.spawn)("xdpyinfo", [], { env, stdio: ["ignore", "pipe", "ignore"] });
390417
390889
  proc.stdout?.on("data", (d) => {
390418
390890
  out += d.toString();
390419
390891
  });
@@ -390431,7 +390903,7 @@ async function detectX11ScreenSize(display) {
390431
390903
  async function checkSystemBinary(name) {
390432
390904
  return new Promise((resolve) => {
390433
390905
  try {
390434
- const proc = (0, import_node_child_process4.spawn)(name, ["-help"], { stdio: "ignore" });
390906
+ const proc = (0, import_node_child_process5.spawn)(name, ["-help"], { stdio: "ignore" });
390435
390907
  proc.on("error", () => resolve(false));
390436
390908
  proc.on("close", () => resolve(true));
390437
390909
  } catch {
@@ -390448,7 +390920,7 @@ async function startXvfb(display, opts = {}) {
390448
390920
  const w = opts.width ?? defW;
390449
390921
  const h = opts.height ?? defH;
390450
390922
  const startMs = Date.now();
390451
- const proc = (0, import_node_child_process4.spawn)("Xvfb", [display, "-screen", "0", `${w}x${h}x24`, "-nolisten", "tcp"], { stdio: "ignore" });
390923
+ const proc = (0, import_node_child_process5.spawn)("Xvfb", [display, "-screen", "0", `${w}x${h}x24`, "-nolisten", "tcp"], { stdio: "ignore" });
390452
390924
  let spawnErr = null;
390453
390925
  proc.on("error", (e) => {
390454
390926
  spawnErr = e;
@@ -390460,7 +390932,7 @@ async function startXvfb(display, opts = {}) {
390460
390932
  if (proc.exitCode !== null)
390461
390933
  throw new Error(`Xvfb exited early on ${display} (code ${proc.exitCode})`);
390462
390934
  try {
390463
- if (import_node_fs10.default.statSync(lock).mtimeMs >= startMs)
390935
+ if (import_node_fs11.default.statSync(lock).mtimeMs >= startMs)
390464
390936
  return proc;
390465
390937
  } catch {
390466
390938
  }
@@ -390545,11 +391017,11 @@ function hasUnresolvedMetaReference(expression, context) {
390545
391017
  }
390546
391018
  return false;
390547
391019
  }
390548
- function getMetaValue(path21, context) {
391020
+ function getMetaValue(path22, context) {
390549
391021
  if (!context) {
390550
391022
  return void 0;
390551
391023
  }
390552
- const [basePath, jsonPointer] = path21.split("#");
391024
+ const [basePath, jsonPointer] = path22.split("#");
390553
391025
  const resolvedPath = resolvePathTemplateVariables(basePath, context);
390554
391026
  let value = getNestedProperty(context, resolvedPath);
390555
391027
  if (jsonPointer && value) {
@@ -390566,9 +391038,9 @@ function getMetaValue(path21, context) {
390566
391038
  }
390567
391039
  return value;
390568
391040
  }
390569
- function resolvePathTemplateVariables(path21, context) {
391041
+ function resolvePathTemplateVariables(path22, context) {
390570
391042
  const templateRegex = /\{\{(\w+)\}\}/g;
390571
- return path21.replace(templateRegex, (match, varName) => {
391043
+ return path22.replace(templateRegex, (match, varName) => {
390572
391044
  if (context && context.id && varName === "id") {
390573
391045
  return context.id;
390574
391046
  }
@@ -390605,10 +391077,10 @@ async function resolveEmbeddedExpressions(str, context) {
390605
391077
  parts.push(str.slice(lastIdx));
390606
391078
  return parts.join("");
390607
391079
  }
390608
- function getNestedProperty(obj, path21) {
390609
- if (!obj || !path21)
391080
+ function getNestedProperty(obj, path22) {
391081
+ if (!obj || !path22)
390610
391082
  return void 0;
390611
- const parts = path21.split(".");
391083
+ const parts = path22.split(".");
390612
391084
  let current = obj;
390613
391085
  for (const part of parts) {
390614
391086
  if (current === null || current === void 0)
@@ -391866,8 +392338,8 @@ async function waitForDOMStable(driver, idleTime, timeout) {
391866
392338
  init_validate();
391867
392339
  init_utils();
391868
392340
  var import_tree_kill = __toESM(require("tree-kill"), 1);
391869
- var import_node_fs11 = __toESM(require("node:fs"), 1);
391870
- var import_node_path11 = __toESM(require("node:path"), 1);
392341
+ var import_node_fs12 = __toESM(require("node:fs"), 1);
392342
+ var import_node_path12 = __toESM(require("node:path"), 1);
391871
392343
  async function runShell({ config, step, processRegistry }) {
391872
392344
  const result = {
391873
392345
  status: "PASS",
@@ -392009,19 +392481,19 @@ async function runShell({ config, step, processRegistry }) {
392009
392481
  descriptions.push(stdioMatched ? `Found expected output (${step.runShell.stdio}) in stdio.` : isRegex ? `Couldn't find expected output (${step.runShell.stdio}) in actual output (stdout or stderr).` : `Couldn't find expected output (${step.runShell.stdio}) in stdio (stdout or stderr).`);
392010
392482
  }
392011
392483
  if (step.runShell.path) {
392012
- const dir = import_node_path11.default.dirname(step.runShell.path);
392013
- if (!import_node_fs11.default.existsSync(dir)) {
392014
- import_node_fs11.default.mkdirSync(dir, { recursive: true });
392484
+ const dir = import_node_path12.default.dirname(step.runShell.path);
392485
+ if (!import_node_fs12.default.existsSync(dir)) {
392486
+ import_node_fs12.default.mkdirSync(dir, { recursive: true });
392015
392487
  }
392016
392488
  let filePath = step.runShell.path;
392017
392489
  log(config, "debug", `Saving stdio to file: ${filePath}`);
392018
- if (!import_node_fs11.default.existsSync(filePath)) {
392019
- import_node_fs11.default.writeFileSync(filePath, result.outputs.stdio.stdout);
392490
+ if (!import_node_fs12.default.existsSync(filePath)) {
392491
+ import_node_fs12.default.writeFileSync(filePath, result.outputs.stdio.stdout);
392020
392492
  } else {
392021
392493
  if (step.runShell.overwrite == "false") {
392022
392494
  descriptions.push(`Didn't save output. File already exists.`);
392023
392495
  }
392024
- const existingFile = import_node_fs11.default.readFileSync(filePath, "utf8");
392496
+ const existingFile = import_node_fs12.default.readFileSync(filePath, "utf8");
392025
392497
  const fractionalDiff = calculateFractionalDifference(existingFile, result.outputs.stdio.stdout);
392026
392498
  log(config, "debug", `Fractional difference: ${fractionalDiff}`);
392027
392499
  result.outputs.variation = fractionalDiff;
@@ -392031,13 +392503,13 @@ async function runShell({ config, step, processRegistry }) {
392031
392503
  });
392032
392504
  if (fractionalDiff > step.runShell.maxVariation) {
392033
392505
  if (step.runShell.overwrite == "aboveVariation") {
392034
- import_node_fs11.default.writeFileSync(filePath, result.outputs.stdio.stdout);
392506
+ import_node_fs12.default.writeFileSync(filePath, result.outputs.stdio.stdout);
392035
392507
  descriptions.push(`Saved output to file.`);
392036
392508
  }
392037
392509
  descriptions.push(`The difference between the existing output and the new output (${fractionalDiff.toFixed(2)}) is greater than the max accepted variation (${step.runShell.maxVariation}).`);
392038
392510
  } else {
392039
392511
  if (step.runShell.overwrite == "true") {
392040
- import_node_fs11.default.writeFileSync(filePath, result.outputs.stdio.stdout);
392512
+ import_node_fs12.default.writeFileSync(filePath, result.outputs.stdio.stdout);
392041
392513
  descriptions.push(`Saved output to file.`);
392042
392514
  }
392043
392515
  descriptions.push(`Saved-file variation (${fractionalDiff.toFixed(2)}) is within the max accepted variation (${step.runShell.maxVariation}).`);
@@ -392235,8 +392707,8 @@ async function checkLink({ config, step }) {
392235
392707
  // dist/core/tests/saveScreenshot.js
392236
392708
  init_validate();
392237
392709
  init_utils();
392238
- var import_node_path12 = __toESM(require("node:path"), 1);
392239
- var import_node_fs12 = __toESM(require("node:fs"), 1);
392710
+ var import_node_path13 = __toESM(require("node:path"), 1);
392711
+ var import_node_fs13 = __toESM(require("node:fs"), 1);
392240
392712
  init_loader();
392241
392713
  var _pngjs = null;
392242
392714
  var _sharp = null;
@@ -392332,7 +392804,7 @@ async function saveScreenshot({ config, step, driver }) {
392332
392804
  if (typeof step.screenshot.path === "undefined") {
392333
392805
  step.screenshot.path = `${step.stepId}.png`;
392334
392806
  if (step.screenshot.directory) {
392335
- step.screenshot.path = import_node_path12.default.resolve(step.screenshot.directory, step.screenshot.path);
392807
+ step.screenshot.path = import_node_path13.default.resolve(step.screenshot.directory, step.screenshot.path);
392336
392808
  }
392337
392809
  }
392338
392810
  step.screenshot = {
@@ -392368,17 +392840,17 @@ async function saveScreenshot({ config, step, driver }) {
392368
392840
  } catch {
392369
392841
  urlPathname = originalUrlPath;
392370
392842
  }
392371
- const rawBase = import_node_path12.default.basename(urlPathname.split("?")[0].split("#")[0].replace(/\\/g, "/"));
392843
+ const rawBase = import_node_path13.default.basename(urlPathname.split("?")[0].split("#")[0].replace(/\\/g, "/"));
392372
392844
  const safeBase = sanitizeFilesystemName(rawBase, `${step.stepId}.png`);
392373
- dir = import_node_path12.default.join(process.cwd(), "doc-detective-runs", getOrInitRunTimestamp(config));
392374
- if (!import_node_fs12.default.existsSync(dir)) {
392375
- import_node_fs12.default.mkdirSync(dir, { recursive: true });
392845
+ dir = import_node_path13.default.join(process.cwd(), "doc-detective-runs", getOrInitRunTimestamp(config));
392846
+ if (!import_node_fs13.default.existsSync(dir)) {
392847
+ import_node_fs13.default.mkdirSync(dir, { recursive: true });
392376
392848
  }
392377
392849
  const captureId = `${step.stepId || "screenshot"}_${Date.now()}`;
392378
- filePath = import_node_path12.default.join(dir, `${captureId}_${safeBase}`);
392379
- const resolvedDir = import_node_path12.default.resolve(dir);
392380
- const resolvedFile = import_node_path12.default.resolve(filePath);
392381
- if (!resolvedFile.startsWith(resolvedDir + import_node_path12.default.sep)) {
392850
+ filePath = import_node_path13.default.join(dir, `${captureId}_${safeBase}`);
392851
+ const resolvedDir = import_node_path13.default.resolve(dir);
392852
+ const resolvedFile = import_node_path13.default.resolve(filePath);
392853
+ if (!resolvedFile.startsWith(resolvedDir + import_node_path13.default.sep)) {
392382
392854
  result.status = "FAIL";
392383
392855
  result.description = `Refusing to write screenshot outside run folder: ${resolvedFile}`;
392384
392856
  return result;
@@ -392387,11 +392859,11 @@ async function saveScreenshot({ config, step, driver }) {
392387
392859
  log(config, "debug", `Screenshot path is a URL (${redactedUrl}); overwrite is ignored, running comparison only.`);
392388
392860
  }
392389
392861
  } else {
392390
- dir = import_node_path12.default.dirname(step.screenshot.path);
392391
- if (!import_node_fs12.default.existsSync(dir)) {
392392
- import_node_fs12.default.mkdirSync(dir, { recursive: true });
392862
+ dir = import_node_path13.default.dirname(step.screenshot.path);
392863
+ if (!import_node_fs13.default.existsSync(dir)) {
392864
+ import_node_fs13.default.mkdirSync(dir, { recursive: true });
392393
392865
  }
392394
- if (import_node_fs12.default.existsSync(filePath)) {
392866
+ if (import_node_fs13.default.existsSync(filePath)) {
392395
392867
  if (step.screenshot.overwrite == "false") {
392396
392868
  result.status = "SKIPPED";
392397
392869
  result.description = `File already exists: ${filePath}`;
@@ -392399,7 +392871,7 @@ async function saveScreenshot({ config, step, driver }) {
392399
392871
  return result;
392400
392872
  } else {
392401
392873
  existFilePath = filePath;
392402
- filePath = import_node_path12.default.join(dir, `${step.stepId}_${Date.now()}.png`);
392874
+ filePath = import_node_path13.default.join(dir, `${step.stepId}_${Date.now()}.png`);
392403
392875
  }
392404
392876
  }
392405
392877
  }
@@ -392497,8 +392969,8 @@ async function saveScreenshot({ config, step, driver }) {
392497
392969
  } catch (error) {
392498
392970
  result.status = "FAIL";
392499
392971
  result.description = `Couldn't save screenshot. ${error}`;
392500
- if (existFilePath && filePath !== existFilePath && import_node_fs12.default.existsSync(filePath)) {
392501
- import_node_fs12.default.unlinkSync(filePath);
392972
+ if (existFilePath && filePath !== existFilePath && import_node_fs13.default.existsSync(filePath)) {
392973
+ import_node_fs13.default.unlinkSync(filePath);
392502
392974
  }
392503
392975
  return result;
392504
392976
  } finally {
@@ -392553,7 +393025,7 @@ async function saveScreenshot({ config, step, driver }) {
392553
393025
  rect.width = clamped.width;
392554
393026
  rect.height = clamped.height;
392555
393027
  log(config, "debug", { padded_rect: rect });
392556
- const croppedPath = import_node_path12.default.join(dir, `cropped_${step.stepId || Date.now()}.png`);
393028
+ const croppedPath = import_node_path13.default.join(dir, `cropped_${step.stepId || Date.now()}.png`);
392557
393029
  try {
392558
393030
  await sharp(filePath).extract({
392559
393031
  left: rect.x,
@@ -392561,12 +393033,12 @@ async function saveScreenshot({ config, step, driver }) {
392561
393033
  width: rect.width,
392562
393034
  height: rect.height
392563
393035
  }).toFile(croppedPath);
392564
- import_node_fs12.default.renameSync(croppedPath, filePath);
393036
+ import_node_fs13.default.renameSync(croppedPath, filePath);
392565
393037
  } catch (error) {
392566
393038
  result.status = "FAIL";
392567
393039
  result.description = `Couldn't crop image. ${error}`;
392568
- if (existFilePath && filePath !== existFilePath && import_node_fs12.default.existsSync(filePath)) {
392569
- import_node_fs12.default.unlinkSync(filePath);
393040
+ if (existFilePath && filePath !== existFilePath && import_node_fs13.default.existsSync(filePath)) {
393041
+ import_node_fs13.default.unlinkSync(filePath);
392570
393042
  }
392571
393043
  return result;
392572
393044
  }
@@ -392574,7 +393046,7 @@ async function saveScreenshot({ config, step, driver }) {
392574
393046
  if (existFilePath) {
392575
393047
  if (step.screenshot.overwrite == "true" && !isUrlPath) {
392576
393048
  result.description += ` Overwrote existing file.`;
392577
- import_node_fs12.default.renameSync(filePath, existFilePath);
393049
+ import_node_fs13.default.renameSync(filePath, existFilePath);
392578
393050
  result.outputs.screenshotPath = existFilePath;
392579
393051
  result.outputs.changed = true;
392580
393052
  if (step.screenshot.sourceIntegration) {
@@ -392587,13 +393059,13 @@ async function saveScreenshot({ config, step, driver }) {
392587
393059
  let img1;
392588
393060
  let img2;
392589
393061
  try {
392590
- img1 = PNG.sync.read(import_node_fs12.default.readFileSync(existFilePath));
392591
- img2 = PNG.sync.read(import_node_fs12.default.readFileSync(filePath));
393062
+ img1 = PNG.sync.read(import_node_fs13.default.readFileSync(existFilePath));
393063
+ img2 = PNG.sync.read(import_node_fs13.default.readFileSync(filePath));
392592
393064
  } catch (error) {
392593
393065
  result.status = "FAIL";
392594
393066
  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}`;
392595
- if (!isUrlPath && filePath !== existFilePath && import_node_fs12.default.existsSync(filePath)) {
392596
- import_node_fs12.default.unlinkSync(filePath);
393067
+ if (!isUrlPath && filePath !== existFilePath && import_node_fs13.default.existsSync(filePath)) {
393068
+ import_node_fs13.default.unlinkSync(filePath);
392597
393069
  }
392598
393070
  return result;
392599
393071
  }
@@ -392605,8 +393077,8 @@ async function saveScreenshot({ config, step, driver }) {
392605
393077
  });
392606
393078
  if (!aspectRatioMatch) {
392607
393079
  result.description = `Couldn't compare images. Images have different aspect ratios.`;
392608
- if (!isUrlPath && filePath !== existFilePath && import_node_fs12.default.existsSync(filePath)) {
392609
- import_node_fs12.default.unlinkSync(filePath);
393080
+ if (!isUrlPath && filePath !== existFilePath && import_node_fs13.default.existsSync(filePath)) {
393081
+ import_node_fs13.default.unlinkSync(filePath);
392610
393082
  }
392611
393083
  return await evaluateApplicable();
392612
393084
  }
@@ -392633,8 +393105,8 @@ async function saveScreenshot({ config, step, driver }) {
392633
393105
  } catch (error) {
392634
393106
  result.status = "FAIL";
392635
393107
  result.description = `Couldn't load screenshot comparison dependency (pixelmatch). ${error?.message ?? error}`;
392636
- if (!isUrlPath && filePath !== existFilePath && import_node_fs12.default.existsSync(filePath)) {
392637
- import_node_fs12.default.unlinkSync(filePath);
393108
+ if (!isUrlPath && filePath !== existFilePath && import_node_fs13.default.existsSync(filePath)) {
393109
+ import_node_fs13.default.unlinkSync(filePath);
392638
393110
  }
392639
393111
  return result;
392640
393112
  }
@@ -392652,7 +393124,7 @@ async function saveScreenshot({ config, step, driver }) {
392652
393124
  });
392653
393125
  if (fractionalDiff > step.screenshot.maxVariation) {
392654
393126
  if (step.screenshot.overwrite == "aboveVariation" && !isUrlPath) {
392655
- import_node_fs12.default.renameSync(filePath, existFilePath);
393127
+ import_node_fs13.default.renameSync(filePath, existFilePath);
392656
393128
  }
392657
393129
  result.description += ` The difference between the existing screenshot and new screenshot (${fractionalDiff.toFixed(2)}) is greater than the max accepted variation (${step.screenshot.maxVariation}).`;
392658
393130
  if (isUrlPath) {
@@ -392677,7 +393149,7 @@ async function saveScreenshot({ config, step, driver }) {
392677
393149
  result.outputs.sourceIntegration = step.screenshot.sourceIntegration;
392678
393150
  }
392679
393151
  if (step.screenshot.overwrite != "true") {
392680
- import_node_fs12.default.unlinkSync(filePath);
393152
+ import_node_fs13.default.unlinkSync(filePath);
392681
393153
  }
392682
393154
  }
392683
393155
  }
@@ -392696,10 +393168,10 @@ async function saveScreenshot({ config, step, driver }) {
392696
393168
  // dist/core/tests/startRecording.js
392697
393169
  init_validate();
392698
393170
  init_utils();
392699
- var import_node_child_process5 = require("node:child_process");
393171
+ var import_node_child_process6 = require("node:child_process");
392700
393172
  var import_node_crypto6 = require("node:crypto");
392701
- var import_node_path13 = __toESM(require("node:path"), 1);
392702
- var import_node_fs13 = __toESM(require("node:fs"), 1);
393173
+ var import_node_path14 = __toESM(require("node:path"), 1);
393174
+ var import_node_fs14 = __toESM(require("node:fs"), 1);
392703
393175
  var import_node_os7 = __toESM(require("node:os"), 1);
392704
393176
  async function startRecording({ config, context, step, driver }) {
392705
393177
  let result = {
@@ -392727,7 +393199,7 @@ async function startRecording({ config, context, step, driver }) {
392727
393199
  if (typeof step.record.path === "undefined") {
392728
393200
  step.record.path = `${step.stepId}.mp4`;
392729
393201
  if (step.record.directory) {
392730
- step.record.path = import_node_path13.default.resolve(step.record.directory, step.record.path);
393202
+ step.record.path = import_node_path14.default.resolve(step.record.directory, step.record.path);
392731
393203
  }
392732
393204
  }
392733
393205
  step.record = {
@@ -392735,18 +393207,18 @@ async function startRecording({ config, context, step, driver }) {
392735
393207
  overwrite: step.record.overwrite || "false"
392736
393208
  };
392737
393209
  let filePath = step.record.path;
392738
- const baseName = import_node_path13.default.basename(filePath, import_node_path13.default.extname(filePath));
392739
- const dir = import_node_path13.default.dirname(step.record.path);
392740
- if (!import_node_fs13.default.existsSync(dir)) {
392741
- import_node_fs13.default.mkdirSync(dir, { recursive: true });
393210
+ const baseName = import_node_path14.default.basename(filePath, import_node_path14.default.extname(filePath));
393211
+ const dir = import_node_path14.default.dirname(step.record.path);
393212
+ if (!import_node_fs14.default.existsSync(dir)) {
393213
+ import_node_fs14.default.mkdirSync(dir, { recursive: true });
392742
393214
  }
392743
- if (import_node_fs13.default.existsSync(filePath) && step.record.overwrite == "false") {
393215
+ if (import_node_fs14.default.existsSync(filePath) && step.record.overwrite == "false") {
392744
393216
  result.status = "SKIPPED";
392745
393217
  result.description = `File already exists: ${filePath}`;
392746
393218
  return result;
392747
393219
  }
392748
393220
  const normalizeActiveTarget = (p) => {
392749
- const resolved = import_node_path13.default.resolve(p);
393221
+ const resolved = import_node_path14.default.resolve(p);
392750
393222
  return process.platform === "win32" || process.platform === "darwin" ? resolved.toLowerCase() : resolved;
392751
393223
  };
392752
393224
  const normalizedTarget = normalizeActiveTarget(filePath);
@@ -392774,13 +393246,13 @@ async function startRecording({ config, context, step, driver }) {
392774
393246
  }
392775
393247
  const captureTitle = browserCaptureTitle(context.contextId);
392776
393248
  const downloadDir = browserDownloadDir(context.contextId);
392777
- if (!import_node_fs13.default.existsSync(downloadDir)) {
392778
- import_node_fs13.default.mkdirSync(downloadDir, { recursive: true });
393249
+ if (!import_node_fs14.default.existsSync(downloadDir)) {
393250
+ import_node_fs14.default.mkdirSync(downloadDir, { recursive: true });
392779
393251
  }
392780
- const downloadPath = import_node_path13.default.join(downloadDir, `${baseName}.webm`);
392781
- if (import_node_fs13.default.existsSync(downloadPath)) {
393252
+ const downloadPath = import_node_path14.default.join(downloadDir, `${baseName}.webm`);
393253
+ if (import_node_fs14.default.existsSync(downloadPath)) {
392782
393254
  try {
392783
- import_node_fs13.default.unlinkSync(downloadPath);
393255
+ import_node_fs14.default.unlinkSync(downloadPath);
392784
393256
  } catch {
392785
393257
  }
392786
393258
  }
@@ -392907,10 +393379,10 @@ async function startRecording({ config, context, step, driver }) {
392907
393379
  } catch {
392908
393380
  }
392909
393381
  }
392910
- const tempDir = import_node_path13.default.join(import_node_os7.default.tmpdir(), "doc-detective", "recordings");
392911
- if (!import_node_fs13.default.existsSync(tempDir))
392912
- import_node_fs13.default.mkdirSync(tempDir, { recursive: true });
392913
- const tempPath = import_node_path13.default.join(tempDir, `${safeContextId(context.contextId)}-${baseName}-${(0, import_node_crypto6.randomUUID)().slice(0, 8)}.mkv`);
393382
+ const tempDir = import_node_path14.default.join(import_node_os7.default.tmpdir(), "doc-detective", "recordings");
393383
+ if (!import_node_fs14.default.existsSync(tempDir))
393384
+ import_node_fs14.default.mkdirSync(tempDir, { recursive: true });
393385
+ const tempPath = import_node_path14.default.join(tempDir, `${safeContextId(context.contextId)}-${baseName}-${(0, import_node_crypto6.randomUUID)().slice(0, 8)}.mkv`);
392914
393386
  let ffmpegPath;
392915
393387
  try {
392916
393388
  ffmpegPath = await getFfmpegPath({ cacheDir: config?.cacheDir });
@@ -392939,7 +393411,7 @@ async function startRecording({ config, context, step, driver }) {
392939
393411
  screenSize
392940
393412
  });
392941
393413
  log(config, "debug", `ffmpeg recording: platform=${process.platform} target=${plan.target}${screenIndex !== void 0 ? ` screen=${screenIndex}` : ""}${context.__display ? ` display=${context.__display}` : ""} -> ${tempPath}`);
392942
- const proc = (0, import_node_child_process5.spawn)(ffmpegPath, args, { stdio: ["pipe", "ignore", "pipe"] });
393414
+ const proc = (0, import_node_child_process6.spawn)(ffmpegPath, args, { stdio: ["pipe", "ignore", "pipe"] });
392943
393415
  let spawnError = null;
392944
393416
  let stderrTail = "";
392945
393417
  proc.stderr?.on("data", (d) => {
@@ -392968,9 +393440,9 @@ async function startRecording({ config, context, step, driver }) {
392968
393440
  // dist/core/tests/stopRecording.js
392969
393441
  init_validate();
392970
393442
  init_utils();
392971
- var import_node_child_process6 = require("node:child_process");
392972
- var import_node_path14 = __toESM(require("node:path"), 1);
392973
- var import_node_fs14 = __toESM(require("node:fs"), 1);
393443
+ var import_node_child_process7 = require("node:child_process");
393444
+ var import_node_path15 = __toESM(require("node:path"), 1);
393445
+ var import_node_fs15 = __toESM(require("node:fs"), 1);
392974
393446
  async function stopRecording({ config, step, driver }) {
392975
393447
  let result = {
392976
393448
  status: "PASS",
@@ -393107,7 +393579,7 @@ async function transcode({ config, sourcePath, targetPath, deleteSource, crop })
393107
393579
  const cy = `max(0\\,min(${crop.y}\\,ih-${ch}))`;
393108
393580
  filters.push(`crop=w=${cw}:h=${ch}:x=${cx}:y=${cy}`);
393109
393581
  }
393110
- if (import_node_path14.default.extname(targetPath) === ".gif") {
393582
+ if (import_node_path15.default.extname(targetPath) === ".gif") {
393111
393583
  filters.push("scale=iw:-1:flags=lanczos");
393112
393584
  }
393113
393585
  if (filters.length > 0) {
@@ -393116,7 +393588,7 @@ async function transcode({ config, sourcePath, targetPath, deleteSource, crop })
393116
393588
  ffmpegArgs.push(`${targetPath}`);
393117
393589
  const ffmpegPath = await getFfmpegPath({ cacheDir: config?.cacheDir });
393118
393590
  await new Promise((resolve, reject) => {
393119
- const child = (0, import_node_child_process6.spawn)(ffmpegPath, ffmpegArgs);
393591
+ const child = (0, import_node_child_process7.spawn)(ffmpegPath, ffmpegArgs);
393120
393592
  let stderr = "";
393121
393593
  child.stderr?.on("data", (d) => {
393122
393594
  stderr = (stderr + d.toString()).slice(-2e3);
@@ -393125,7 +393597,7 @@ async function transcode({ config, sourcePath, targetPath, deleteSource, crop })
393125
393597
  if (code === 0) {
393126
393598
  if (deleteSource && sourcePath !== targetPath) {
393127
393599
  try {
393128
- import_node_fs14.default.unlinkSync(sourcePath);
393600
+ import_node_fs15.default.unlinkSync(sourcePath);
393129
393601
  } catch {
393130
393602
  }
393131
393603
  }
@@ -393144,7 +393616,7 @@ async function waitForStableFile(filePath, maxSeconds) {
393144
393616
  for (let i = 0; i < deadline; i++) {
393145
393617
  let size = -1;
393146
393618
  try {
393147
- size = import_node_fs14.default.statSync(filePath).size;
393619
+ size = import_node_fs15.default.statSync(filePath).size;
393148
393620
  } catch {
393149
393621
  size = -1;
393150
393622
  }
@@ -393184,8 +393656,8 @@ async function loadVariables({ step }) {
393184
393656
  // dist/core/tests/saveCookie.js
393185
393657
  init_validate();
393186
393658
  init_utils();
393187
- var import_node_path15 = __toESM(require("node:path"), 1);
393188
- var import_node_fs15 = __toESM(require("node:fs"), 1);
393659
+ var import_node_path16 = __toESM(require("node:path"), 1);
393660
+ var import_node_fs16 = __toESM(require("node:fs"), 1);
393189
393661
  async function saveCookie({ config, step, driver }) {
393190
393662
  let result = {
393191
393663
  status: "PASS",
@@ -393202,7 +393674,7 @@ async function saveCookie({ config, step, driver }) {
393202
393674
  let cookieName, filePath, directory, overwrite, domain, variable;
393203
393675
  if (typeof step.saveCookie === "string") {
393204
393676
  if (step.saveCookie.endsWith(".txt")) {
393205
- cookieName = import_node_path15.default.basename(step.saveCookie, ".txt");
393677
+ cookieName = import_node_path16.default.basename(step.saveCookie, ".txt");
393206
393678
  filePath = step.saveCookie;
393207
393679
  } else {
393208
393680
  cookieName = step.saveCookie;
@@ -393251,15 +393723,15 @@ async function saveCookie({ config, step, driver }) {
393251
393723
  }
393252
393724
  if (filePath) {
393253
393725
  const outputDirectory = directory || config.output || process.cwd();
393254
- const fullPath = import_node_path15.default.resolve(outputDirectory, filePath);
393255
- if (import_node_fs15.default.existsSync(fullPath) && !(overwrite === true || overwrite === "true")) {
393726
+ const fullPath = import_node_path16.default.resolve(outputDirectory, filePath);
393727
+ if (import_node_fs16.default.existsSync(fullPath) && !(overwrite === true || overwrite === "true")) {
393256
393728
  result.status = "FAIL";
393257
393729
  result.description = `File '${fullPath}' already exists and overwrite is not enabled.`;
393258
393730
  return result;
393259
393731
  }
393260
- const dir = import_node_path15.default.dirname(fullPath);
393261
- if (!import_node_fs15.default.existsSync(dir)) {
393262
- import_node_fs15.default.mkdirSync(dir, { recursive: true });
393732
+ const dir = import_node_path16.default.dirname(fullPath);
393733
+ if (!import_node_fs16.default.existsSync(dir)) {
393734
+ import_node_fs16.default.mkdirSync(dir, { recursive: true });
393263
393735
  }
393264
393736
  if (targetCookie) {
393265
393737
  const netscapeCookie = formatCookieForNetscape(targetCookie);
@@ -393267,11 +393739,11 @@ async function saveCookie({ config, step, driver }) {
393267
393739
  # This is a cookie file saved by Doc Detective
393268
393740
  ${netscapeCookie}
393269
393741
  `;
393270
- import_node_fs15.default.writeFileSync(fullPath, content, "utf8");
393742
+ import_node_fs16.default.writeFileSync(fullPath, content, "utf8");
393271
393743
  result.description = `Saved cookie '${cookieName}' to '${fullPath}'.`;
393272
393744
  log(config, "debug", `Saved cookie '${cookieName}' to file: ${fullPath}`);
393273
393745
  } else {
393274
- import_node_fs15.default.writeFileSync(fullPath, "# No cookie data\n", "utf8");
393746
+ import_node_fs16.default.writeFileSync(fullPath, "# No cookie data\n", "utf8");
393275
393747
  result.description = `Created empty cookie file at '${fullPath}'.`;
393276
393748
  log(config, "debug", `Created empty cookie file: ${fullPath}`);
393277
393749
  }
@@ -393298,8 +393770,8 @@ function formatCookieForNetscape(cookie) {
393298
393770
  // dist/core/tests/loadCookie.js
393299
393771
  init_validate();
393300
393772
  init_utils();
393301
- var import_node_path16 = __toESM(require("node:path"), 1);
393302
- var import_node_fs16 = __toESM(require("node:fs"), 1);
393773
+ var import_node_path17 = __toESM(require("node:path"), 1);
393774
+ var import_node_fs17 = __toESM(require("node:fs"), 1);
393303
393775
  async function loadCookie({ config, step, driver }) {
393304
393776
  let result = {
393305
393777
  status: "PASS",
@@ -393316,7 +393788,7 @@ async function loadCookie({ config, step, driver }) {
393316
393788
  let cookieName, filePath, directory, domain, variable;
393317
393789
  if (typeof step.loadCookie === "string") {
393318
393790
  if (step.loadCookie.endsWith(".txt")) {
393319
- cookieName = import_node_path16.default.basename(step.loadCookie, ".txt");
393791
+ cookieName = import_node_path17.default.basename(step.loadCookie, ".txt");
393320
393792
  filePath = step.loadCookie;
393321
393793
  } else {
393322
393794
  cookieName = step.loadCookie;
@@ -393329,9 +393801,9 @@ async function loadCookie({ config, step, driver }) {
393329
393801
  domain = step.loadCookie.domain;
393330
393802
  variable = step.loadCookie.variable;
393331
393803
  if (filePath && !cookieName) {
393332
- const ext = import_node_path16.default.extname(filePath).toLowerCase();
393804
+ const ext = import_node_path17.default.extname(filePath).toLowerCase();
393333
393805
  if (ext === ".txt") {
393334
- cookieName = import_node_path16.default.basename(filePath, ext);
393806
+ cookieName = import_node_path17.default.basename(filePath, ext);
393335
393807
  }
393336
393808
  }
393337
393809
  }
@@ -393354,14 +393826,14 @@ async function loadCookie({ config, step, driver }) {
393354
393826
  }
393355
393827
  } else if (filePath) {
393356
393828
  const inputDirectory = directory || config.output || process.cwd();
393357
- const fullPath = import_node_path16.default.resolve(inputDirectory, filePath);
393358
- if (!import_node_fs16.default.existsSync(fullPath)) {
393829
+ const fullPath = import_node_path17.default.resolve(inputDirectory, filePath);
393830
+ if (!import_node_fs17.default.existsSync(fullPath)) {
393359
393831
  result.status = "FAIL";
393360
393832
  result.description = `Cookie file '${fullPath}' not found`;
393361
393833
  return result;
393362
393834
  }
393363
393835
  try {
393364
- const fileContent = import_node_fs16.default.readFileSync(fullPath, "utf8");
393836
+ const fileContent = import_node_fs17.default.readFileSync(fullPath, "utf8");
393365
393837
  const cookies = parseNetscapeCookieFile(fileContent);
393366
393838
  if (cookies.length === 0) {
393367
393839
  result.status = "FAIL";
@@ -393527,8 +393999,8 @@ function isDomainCompatible(currentDomain, cookieDomain) {
393527
393999
  // dist/core/tests/httpRequest.js
393528
394000
  init_validate();
393529
394001
  var import_axios5 = __toESM(require("axios"), 1);
393530
- var import_node_fs17 = __toESM(require("node:fs"), 1);
393531
- var import_node_path17 = __toESM(require("node:path"), 1);
394002
+ var import_node_fs18 = __toESM(require("node:fs"), 1);
394003
+ var import_node_path18 = __toESM(require("node:path"), 1);
393532
394004
  var import_ajv2 = __toESM(require("ajv"), 1);
393533
394005
  init_openapi();
393534
394006
  init_utils();
@@ -393555,9 +394027,9 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
393555
394027
  delete step.httpRequest.openApi.definition;
393556
394028
  } else if (openApiDefinitions.length > 0) {
393557
394029
  for (const openApiConfig of openApiDefinitions) {
393558
- for (const path21 in openApiConfig.definition.paths) {
393559
- for (const method in openApiConfig.definition.paths[path21]) {
393560
- if (openApiConfig.definition.paths[path21][method].operationId === step.httpRequest.openApi.operationId) {
394030
+ for (const path22 in openApiConfig.definition.paths) {
394031
+ for (const method in openApiConfig.definition.paths[path22]) {
394032
+ if (openApiConfig.definition.paths[path22][method].operationId === step.httpRequest.openApi.operationId) {
393561
394033
  openApiDefinition = openApiConfig.definition;
393562
394034
  step.httpRequest.openApi = {
393563
394035
  ...openApiConfig,
@@ -393859,20 +394331,20 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
393859
394331
  }
393860
394332
  }
393861
394333
  if (step.httpRequest.path) {
393862
- const dir = import_node_path17.default.dirname(step.httpRequest.path);
393863
- if (!import_node_fs17.default.existsSync(dir)) {
393864
- import_node_fs17.default.mkdirSync(dir, { recursive: true });
394334
+ const dir = import_node_path18.default.dirname(step.httpRequest.path);
394335
+ if (!import_node_fs18.default.existsSync(dir)) {
394336
+ import_node_fs18.default.mkdirSync(dir, { recursive: true });
393865
394337
  }
393866
394338
  let filePath = step.httpRequest.path;
393867
394339
  log(config, "debug", `Saving output to file: ${filePath}`);
393868
- if (!import_node_fs17.default.existsSync(filePath)) {
393869
- await import_node_fs17.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
394340
+ if (!import_node_fs18.default.existsSync(filePath)) {
394341
+ await import_node_fs18.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
393870
394342
  descriptions.push(`Saved output to file.`);
393871
394343
  } else {
393872
394344
  if (step.httpRequest.overwrite == "false") {
393873
394345
  descriptions.push(`Didn't save output. File already exists.`);
393874
394346
  }
393875
- const existingFile = import_node_fs17.default.readFileSync(filePath, "utf8");
394347
+ const existingFile = import_node_fs18.default.readFileSync(filePath, "utf8");
393876
394348
  const fractionalDiff = calculateFractionalDifference(existingFile, JSON.stringify(response.data, null, 2));
393877
394349
  log(config, "debug", `Fractional difference: ${fractionalDiff}`);
393878
394350
  result.outputs.variation = fractionalDiff;
@@ -393882,13 +394354,13 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
393882
394354
  });
393883
394355
  if (fractionalDiff > step.httpRequest.maxVariation) {
393884
394356
  if (step.httpRequest.overwrite == "aboveVariation") {
393885
- await import_node_fs17.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
394357
+ await import_node_fs18.default.promises.writeFile(filePath, JSON.stringify(response.data, null, 2));
393886
394358
  descriptions.push(`Saved response to file.`);
393887
394359
  }
393888
394360
  descriptions.push(`The difference between the existing saved response and the new response (${fractionalDiff.toFixed(2)}) is greater than the max accepted variation (${step.httpRequest.maxVariation}).`);
393889
394361
  } else {
393890
394362
  if (step.httpRequest.overwrite == "true") {
393891
- import_node_fs17.default.writeFileSync(filePath, JSON.stringify(response.data, null, 2));
394363
+ import_node_fs18.default.writeFileSync(filePath, JSON.stringify(response.data, null, 2));
393892
394364
  descriptions.push(`Saved response to file.`);
393893
394365
  }
393894
394366
  }
@@ -393901,8 +394373,8 @@ async function httpRequest({ config, step, openApiDefinitions = [] }) {
393901
394373
  result.description = descriptions.join(" ").trim();
393902
394374
  return result;
393903
394375
  }
393904
- function fieldExistsAtPath(obj, path21) {
393905
- const segments = path21.match(/[^.[\]]+/g);
394376
+ function fieldExistsAtPath(obj, path22) {
394377
+ const segments = path22.match(/[^.[\]]+/g);
393906
394378
  if (!segments) {
393907
394379
  return false;
393908
394380
  }
@@ -394071,8 +394543,8 @@ async function clickElement({ config, step, driver }) {
394071
394543
  // dist/core/tests/runCode.js
394072
394544
  init_validate();
394073
394545
  init_utils();
394074
- var import_node_fs18 = __toESM(require("node:fs"), 1);
394075
- var import_node_path18 = __toESM(require("node:path"), 1);
394546
+ var import_node_fs19 = __toESM(require("node:fs"), 1);
394547
+ var import_node_path19 = __toESM(require("node:path"), 1);
394076
394548
  var import_node_os8 = __toESM(require("node:os"), 1);
394077
394549
  function createTempScript(code, language) {
394078
394550
  let extension;
@@ -394093,9 +394565,9 @@ function createTempScript(code, language) {
394093
394565
  extension = "";
394094
394566
  }
394095
394567
  const tmpDir = import_node_os8.default.tmpdir();
394096
- const tmpFile = import_node_path18.default.join(tmpDir, `doc-detective-${Date.now()}${extension}`);
394568
+ const tmpFile = import_node_path19.default.join(tmpDir, `doc-detective-${Date.now()}${extension}`);
394097
394569
  try {
394098
- import_node_fs18.default.writeFileSync(tmpFile, code);
394570
+ import_node_fs19.default.writeFileSync(tmpFile, code);
394099
394571
  } catch (error) {
394100
394572
  throw new Error(`Failed to create temporary script: ${error.message}`);
394101
394573
  }
@@ -394176,7 +394648,7 @@ async function runCode({ config, step, processRegistry }) {
394176
394648
  if (typeof step.runCode.stdio !== "undefined")
394177
394649
  runShellOptions.stdio = step.runCode.stdio;
394178
394650
  if (typeof step.runCode.path !== "undefined") {
394179
- runShellOptions.path = step.runCode.directory ? import_node_path18.default.join(step.runCode.directory, step.runCode.path) : step.runCode.path;
394651
+ runShellOptions.path = step.runCode.directory ? import_node_path19.default.join(step.runCode.directory, step.runCode.path) : step.runCode.path;
394180
394652
  }
394181
394653
  if (step.runCode.background) {
394182
394654
  runShellOptions.background = step.runCode.background;
@@ -394204,7 +394676,7 @@ async function runCode({ config, step, processRegistry }) {
394204
394676
  } finally {
394205
394677
  if (!deferTempCleanup) {
394206
394678
  try {
394207
- import_node_fs18.default.unlinkSync(scriptPath);
394679
+ import_node_fs19.default.unlinkSync(scriptPath);
394208
394680
  log(config, "debug", `Removed temporary script: ${scriptPath}`);
394209
394681
  } catch (error) {
394210
394682
  log(config, "warning", `Failed to remove temporary script: ${scriptPath}`);
@@ -394218,7 +394690,7 @@ async function runCode({ config, step, processRegistry }) {
394218
394690
  init_validate();
394219
394691
  init_utils();
394220
394692
  var import_tree_kill2 = __toESM(require("tree-kill"), 1);
394221
- var import_node_fs19 = __toESM(require("node:fs"), 1);
394693
+ var import_node_fs20 = __toESM(require("node:fs"), 1);
394222
394694
  function resolveSurfaceNames(ref) {
394223
394695
  const items = Array.isArray(ref) ? ref : [ref];
394224
394696
  const names = [];
@@ -394265,7 +394737,7 @@ async function closeSurface({ config, step, processRegistry }) {
394265
394737
  }
394266
394738
  if (entry.tempPath) {
394267
394739
  try {
394268
- import_node_fs19.default.unlinkSync(entry.tempPath);
394740
+ import_node_fs20.default.unlinkSync(entry.tempPath);
394269
394741
  } catch {
394270
394742
  }
394271
394743
  }
@@ -394291,8 +394763,8 @@ async function closeSurface({ config, step, processRegistry }) {
394291
394763
  // dist/core/tests/runBrowserScript.js
394292
394764
  init_validate();
394293
394765
  init_utils();
394294
- var import_node_fs20 = __toESM(require("node:fs"), 1);
394295
- var import_node_path19 = __toESM(require("node:path"), 1);
394766
+ var import_node_fs21 = __toESM(require("node:fs"), 1);
394767
+ var import_node_path20 = __toESM(require("node:path"), 1);
394296
394768
  async function runBrowserScript({ config, step, driver }) {
394297
394769
  const result = {
394298
394770
  status: "PASS",
@@ -394358,16 +394830,16 @@ async function runBrowserScript({ config, step, driver }) {
394358
394830
  }
394359
394831
  if (step.runBrowserScript.path) {
394360
394832
  try {
394361
- const dir = import_node_path19.default.dirname(step.runBrowserScript.path);
394362
- if (!import_node_fs20.default.existsSync(dir)) {
394363
- import_node_fs20.default.mkdirSync(dir, { recursive: true });
394833
+ const dir = import_node_path20.default.dirname(step.runBrowserScript.path);
394834
+ if (!import_node_fs21.default.existsSync(dir)) {
394835
+ import_node_fs21.default.mkdirSync(dir, { recursive: true });
394364
394836
  }
394365
394837
  const filePath = step.runBrowserScript.path;
394366
394838
  log(config, "debug", `Saving script result to file: ${filePath}`);
394367
- if (!import_node_fs20.default.existsSync(filePath)) {
394368
- import_node_fs20.default.writeFileSync(filePath, serialized);
394839
+ if (!import_node_fs21.default.existsSync(filePath)) {
394840
+ import_node_fs21.default.writeFileSync(filePath, serialized);
394369
394841
  } else {
394370
- const existingFile = import_node_fs20.default.readFileSync(filePath, "utf8");
394842
+ const existingFile = import_node_fs21.default.readFileSync(filePath, "utf8");
394371
394843
  const fractionalDiff = calculateFractionalDifference(existingFile, serialized);
394372
394844
  log(config, "debug", `Fractional difference: ${fractionalDiff}`);
394373
394845
  result.outputs.variation = fractionalDiff;
@@ -394377,7 +394849,7 @@ async function runBrowserScript({ config, step, driver }) {
394377
394849
  });
394378
394850
  if (fractionalDiff > step.runBrowserScript.maxVariation) {
394379
394851
  if (step.runBrowserScript.overwrite == "aboveVariation" || step.runBrowserScript.overwrite == "true") {
394380
- import_node_fs20.default.writeFileSync(filePath, serialized);
394852
+ import_node_fs21.default.writeFileSync(filePath, serialized);
394381
394853
  descriptions.push(`Saved output to file.`);
394382
394854
  } else {
394383
394855
  descriptions.push(`Didn't overwrite the existing file.`);
@@ -394385,7 +394857,7 @@ async function runBrowserScript({ config, step, driver }) {
394385
394857
  descriptions.push(`The difference between the existing output and the new output (${fractionalDiff.toFixed(2)}) is greater than the max accepted variation (${step.runBrowserScript.maxVariation}).`);
394386
394858
  } else {
394387
394859
  if (step.runBrowserScript.overwrite == "true") {
394388
- import_node_fs20.default.writeFileSync(filePath, serialized);
394860
+ import_node_fs21.default.writeFileSync(filePath, serialized);
394389
394861
  descriptions.push(`Saved output to file.`);
394390
394862
  }
394391
394863
  }
@@ -394534,8 +395006,8 @@ async function dragAndDropElement({ config, step, driver }) {
394534
395006
  }
394535
395007
 
394536
395008
  // dist/core/tests.js
394537
- var import_node_path20 = __toESM(require("node:path"), 1);
394538
- var import_node_child_process7 = require("node:child_process");
395009
+ var import_node_path21 = __toESM(require("node:path"), 1);
395010
+ var import_node_child_process8 = require("node:child_process");
394539
395011
  var import_node_crypto7 = require("node:crypto");
394540
395012
  init_appium();
394541
395013
  init_config();
@@ -394688,7 +395160,7 @@ function getIntegrationConfig(config, sourceIntegration) {
394688
395160
  var import_node_http2 = __toESM(require("node:http"), 1);
394689
395161
  var import_node_https2 = __toESM(require("node:https"), 1);
394690
395162
  var import_node_url6 = require("node:url");
394691
- var __dirname5 = import_node_path20.default.dirname((0, import_node_url6.fileURLToPath)(importMetaUrl));
395163
+ var __dirname5 = import_node_path21.default.dirname((0, import_node_url6.fileURLToPath)(importMetaUrl));
394692
395164
  var KNOWN_BROWSERS = ["firefox", "chrome", "safari", "webkit"];
394693
395165
  function combinationKey(context) {
394694
395166
  const rawName = context?.browser?.name;
@@ -394835,6 +395307,49 @@ function getDefaultBrowser({ runnerDetails }) {
394835
395307
  }
394836
395308
  return browser;
394837
395309
  }
395310
+ function normalizeBrowserName(name) {
395311
+ return name === "webkit" ? "safari" : name ?? "";
395312
+ }
395313
+ function buildFallbackCandidates({ requestedName, explicit, policy, availableApps }) {
395314
+ const available = new Set((availableApps || []).map((a) => a.name));
395315
+ const requestedNorm = normalizeBrowserName(requestedName);
395316
+ const candidates = [];
395317
+ if (available.has(requestedNorm))
395318
+ candidates.push(requestedName);
395319
+ const fallbackAllowed = policy === "auto" || policy === "explicit" && !explicit;
395320
+ if (fallbackAllowed) {
395321
+ for (const name of ["firefox", "chrome", "safari"]) {
395322
+ if (name === requestedNorm)
395323
+ continue;
395324
+ if (available.has(name))
395325
+ candidates.push(name);
395326
+ }
395327
+ }
395328
+ return candidates;
395329
+ }
395330
+ function resolveBrowserFallbackPolicy({ context, config }) {
395331
+ return context?.browserFallback || config?.browserFallback || "auto";
395332
+ }
395333
+ function shouldRepairBeforeFallback({ candidateName, requestedName, installAttempts }) {
395334
+ if (normalizeBrowserName(candidateName) !== normalizeBrowserName(requestedName)) {
395335
+ return false;
395336
+ }
395337
+ if (requiredBrowserAssets(candidateName).length === 0)
395338
+ return false;
395339
+ return !installAttempts.has((candidateName ?? "<none>").toLowerCase());
395340
+ }
395341
+ function driverSkipDiagnostic({ requestedName, platform, platformMatches, attemptedFallback, lastError }) {
395342
+ if (!platformMatches) {
395343
+ return `Skipping context on '${platform}': this context targets a different platform.`;
395344
+ }
395345
+ const driverHint = normalizeBrowserName(requestedName) === "firefox" ? "geckodriver" : normalizeBrowserName(requestedName) === "chrome" ? "chromedriver" : normalizeBrowserName(requestedName) === "safari" ? "safaridriver" : "driver";
395346
+ let msg = `Skipping context: could not start a browser session for '${requestedName}' on '${platform}'`;
395347
+ msg += attemptedFallback ? `, and no other available browser could start either.` : ` and cross-browser fallback is disabled or unavailable.`;
395348
+ if (lastError)
395349
+ msg += ` Last error: ${lastError}`;
395350
+ msg += ` A present-but-broken driver (for example a partially downloaded ${driverHint}) can cause this; reinstall the driver or its browser.`;
395351
+ return msg;
395352
+ }
394838
395353
  async function setViewportSize(context, driver) {
394839
395354
  if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
394840
395355
  const viewportSize = await driver.execute("return { width: window.innerWidth, height: window.innerHeight }", []);
@@ -394983,7 +395498,7 @@ async function runSpecs({ resolvedTests }) {
394983
395498
  const runDir = getRunOutputDir(config, {
394984
395499
  create: runArchivesArtifacts(config, specs)
394985
395500
  });
394986
- const runId = import_node_path20.default.basename(runDir).replace(/^run-/, "");
395501
+ const runId = import_node_path21.default.basename(runDir).replace(/^run-/, "");
394987
395502
  const report = {
394988
395503
  runId,
394989
395504
  runDir,
@@ -395218,7 +395733,7 @@ async function runSpecs({ resolvedTests }) {
395218
395733
  }
395219
395734
  if (entry?.tempPath) {
395220
395735
  try {
395221
- import_node_fs21.default.unlinkSync(entry.tempPath);
395736
+ import_node_fs22.default.unlinkSync(entry.tempPath);
395222
395737
  } catch {
395223
395738
  }
395224
395739
  }
@@ -395607,7 +396122,9 @@ async function warmUpContexts({ jobs, config, runnerDetails, appiumPool, install
395607
396122
  deps: {
395608
396123
  ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
395609
396124
  log
395610
- }
396125
+ },
396126
+ // Repair a present-but-broken driver, not just install-if-missing.
396127
+ repair: true
395611
396128
  });
395612
396129
  if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
395613
396130
  clearAppCache(config);
@@ -395775,7 +396292,7 @@ function buildAutoRecordStep({ config, spec, test, context }) {
395775
396292
  return null;
395776
396293
  const runDir = getRunOutputDir(config, { create: false });
395777
396294
  const fileName = `${capPathSegment(sanitizeFilesystemName(String(context.contextId ?? ""), "context"))}.mp4`;
395778
- const recordPath = import_node_path20.default.join(runDir, "recordings", capPathSegment(sanitizeFilesystemName(String(spec.specId ?? ""), "spec")), capPathSegment(sanitizeFilesystemName(String(test.testId ?? ""), "test")), fileName);
396295
+ const recordPath = import_node_path21.default.join(runDir, "recordings", capPathSegment(sanitizeFilesystemName(String(spec.specId ?? ""), "spec")), capPathSegment(sanitizeFilesystemName(String(test.testId ?? ""), "test")), fileName);
395779
396296
  return {
395780
396297
  record: { path: recordPath, overwrite: "true", engine: "ffmpeg" },
395781
396298
  description: "Automatic full-context recording",
@@ -395793,7 +396310,7 @@ async function captureAutoScreenshot({ config, driver, spec, test, context, step
395793
396310
  const action = BROWSER_STEP_KEYS.find((key) => typeof step[key] !== "undefined") || "step";
395794
396311
  const sanitizedTestId = sanitizeFilesystemName(String(test.testId ?? ""), "test");
395795
396312
  const runDir = getRunOutputDir(config);
395796
- const dir = import_node_path20.default.join(runDir, "screenshots", capPathSegment(sanitizeFilesystemName(String(spec.specId ?? ""), "spec")), capPathSegment(sanitizedTestId), capPathSegment(sanitizeFilesystemName(String(context.contextId ?? ""), "context")));
396313
+ const dir = import_node_path21.default.join(runDir, "screenshots", capPathSegment(sanitizeFilesystemName(String(spec.specId ?? ""), "spec")), capPathSegment(sanitizedTestId), capPathSegment(sanitizeFilesystemName(String(context.contextId ?? ""), "context")));
395797
396314
  const stepIdString = sanitizeFilesystemName(String(step.stepId ?? ""), "step");
395798
396315
  const stepRef = capPathSegment(stepIdString.startsWith(`${sanitizedTestId}~`) ? stepIdString.slice(sanitizedTestId.length + 1) : stepIdString);
395799
396316
  const pad = Math.max(2, String(stepCount).length);
@@ -395802,7 +396319,7 @@ async function captureAutoScreenshot({ config, driver, spec, test, context, step
395802
396319
  stepId: `${step.stepId}_auto`,
395803
396320
  description: "Automatic post-step screenshot",
395804
396321
  screenshot: {
395805
- path: import_node_path20.default.join(dir, fileName),
396322
+ path: import_node_path21.default.join(dir, fileName),
395806
396323
  overwrite: "true"
395807
396324
  }
395808
396325
  };
@@ -395815,7 +396332,7 @@ async function captureAutoScreenshot({ config, driver, spec, test, context, step
395815
396332
  log(config, "warning", `Auto screenshot failed after step ${step.stepId}: ${captureResult.description}`);
395816
396333
  return null;
395817
396334
  }
395818
- return import_node_path20.default.relative(runDir, screenshotStep.screenshot.path).split(import_node_path20.default.sep).join("/");
396335
+ return import_node_path21.default.relative(runDir, screenshotStep.screenshot.path).split(import_node_path21.default.sep).join("/");
395819
396336
  } catch (error) {
395820
396337
  log(config, "warning", `Auto screenshot failed after step ${step.stepId}: ${error?.message ?? error}`);
395821
396338
  return null;
@@ -395859,6 +396376,7 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
395859
396376
  platform
395860
396377
  });
395861
396378
  let freshInstallRedetected = false;
396379
+ let freshInstallOutcome;
395862
396380
  if (!supportedContext && context.platform === platform && // Mirror isSupportedContext's own guard: isDriverRequired iterates
395863
396381
  // context.steps, so a malformed context without a steps array would
395864
396382
  // otherwise crash here instead of skipping cleanly.
@@ -395871,10 +396389,15 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
395871
396389
  deps: {
395872
396390
  ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
395873
396391
  log
395874
- }
396392
+ },
396393
+ // The browser is unavailable here — possibly because its driver is
396394
+ // present-but-broken — so repair (force a clean driver reinstall +
396395
+ // re-validation), not just install-if-missing.
396396
+ repair: true
395875
396397
  });
395876
396398
  if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
395877
396399
  freshInstallRedetected = true;
396400
+ freshInstallOutcome = outcome;
395878
396401
  clearAppCache(config);
395879
396402
  availableApps = await getAvailableApps({ config });
395880
396403
  runnerDetails.availableApps = availableApps;
@@ -395885,9 +396408,35 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
395885
396408
  });
395886
396409
  }
395887
396410
  }
395888
- if (!supportedContext) {
395889
- 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)}}`;
395890
- clog(freshInstallRedetected ? "warning" : "info", errorMessage);
396411
+ const platformMatches = context.platform === platform;
396412
+ const requestedBrowserName = context.browser?.name;
396413
+ const explicitlyRequested = context.browser?.explicit === true;
396414
+ const fallbackPolicy = resolveBrowserFallbackPolicy({ context, config });
396415
+ const driverRequired = isDriverRequired({ test: context });
396416
+ const candidateEngines = platformMatches && driverRequired ? buildFallbackCandidates({
396417
+ requestedName: requestedBrowserName,
396418
+ explicit: explicitlyRequested,
396419
+ policy: fallbackPolicy,
396420
+ availableApps
396421
+ }) : [];
396422
+ if (driverRequired && candidateEngines.length === 0) {
396423
+ const errorMessage = freshInstallRedetected ? freshInstallOutcome === "installed" ? `Skipping context '${requestedBrowserName}' on '${context.platform}': the missing browser dependency was installed but still could not be detected.` : `Skipping context '${requestedBrowserName}' on '${context.platform}': the on-demand install/repair of its browser dependency failed.` : driverSkipDiagnostic({
396424
+ requestedName: requestedBrowserName ?? "<none>",
396425
+ // driverSkipDiagnostic treats `platform` as the *current* runner
396426
+ // platform (as in the !startedName path), so pass that — not the
396427
+ // context's target platform — or the mismatch message mislabels it.
396428
+ platform,
396429
+ platformMatches,
396430
+ attemptedFallback: false
396431
+ });
396432
+ clog(platformMatches ? "warning" : "info", errorMessage);
396433
+ contextReport.result = "SKIPPED";
396434
+ contextReport.resultDescription = errorMessage;
396435
+ return contextReport;
396436
+ }
396437
+ if (!driverRequired && !platformMatches) {
396438
+ const errorMessage = `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`;
396439
+ clog("info", errorMessage);
395891
396440
  contextReport.result = "SKIPPED";
395892
396441
  contextReport.resultDescription = errorMessage;
395893
396442
  return contextReport;
@@ -395896,20 +396445,13 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
395896
396445
  ${JSON.stringify(context, null, 2)}`);
395897
396446
  let driver;
395898
396447
  let appiumPort;
395899
- const driverRequired = isDriverRequired({ test: context });
396448
+ let fellBackNote = "";
396449
+ let fellBackPinned = false;
395900
396450
  if (driverRequired && !appiumPool) {
395901
396451
  throw new Error("Driver requested but no Appium server pool was created; driverJobCount and isDriverRequired(context) disagreed; this is a bug.");
395902
396452
  }
395903
- const combo = combinationKey(context);
395904
396453
  try {
395905
396454
  if (driverRequired) {
395906
- if (warmUpDecision(warmUpResults.get(combo)) === "skip") {
395907
- const errorMessage = `Skipping context '${context.browser?.name}' on '${context.platform}': this context combination could not start a driver earlier in this run.`;
395908
- clog("warning", errorMessage);
395909
- contextReport.result = "SKIPPED";
395910
- contextReport.resultDescription = errorMessage;
395911
- return contextReport;
395912
- }
395913
396455
  appiumPort = await appiumPool.acquire();
395914
396456
  if (portToDisplay) {
395915
396457
  const display = portToDisplay.get(appiumPort);
@@ -395922,49 +396464,118 @@ ${JSON.stringify(context, null, 2)}`);
395922
396464
  captureSourceTitle: browserCaptureTitle(context.contextId),
395923
396465
  downloadDir: browserDownloadDir(context.contextId)
395924
396466
  };
395925
- let caps = getDriverCapabilities({
395926
- runnerDetails,
395927
- name: context.browser.name,
395928
- options: {
395929
- width: context.browser?.window?.width || 1200,
395930
- height: context.browser?.window?.height || 800,
395931
- headless: context.browser?.headless !== false,
395932
- ...recordOptions
395933
- }
395934
- });
395935
- clog("debug", "CAPABILITIES:");
395936
- clog("debug", caps);
395937
- try {
395938
- driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
395939
- } catch (error) {
396467
+ const startDriverForBrowser = async (browserName) => {
396468
+ const wantHeadless = context.browser?.headless !== false;
396469
+ const buildCaps = (headless) => getDriverCapabilities({
396470
+ runnerDetails,
396471
+ name: browserName,
396472
+ options: {
396473
+ width: context.browser?.window?.width || 1200,
396474
+ height: context.browser?.window?.height || 800,
396475
+ headless,
396476
+ ...recordOptions
396477
+ }
396478
+ });
396479
+ const startFailure = () => {
396480
+ let error = `Failed to start context '${browserName}' on '${platform}'.`;
396481
+ if (browserName === "safari" || browserName === "webkit") {
396482
+ error += " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
396483
+ }
396484
+ return { ok: false, error };
396485
+ };
395940
396486
  try {
395941
- clog("warning", `Failed to start context '${context.browser?.name}' on '${platform}'. Retrying as headless.`);
395942
- context.browser.headless = true;
395943
- caps = getDriverCapabilities({
395944
- runnerDetails,
395945
- name: context.browser.name,
395946
- options: {
395947
- width: context.browser?.window?.width || 1200,
395948
- height: context.browser?.window?.height || 800,
395949
- headless: context.browser?.headless !== false,
395950
- ...recordOptions
395951
- }
396487
+ const d = await driverStart(buildCaps(wantHeadless), appiumPort, 4, {
396488
+ cacheDir: config?.cacheDir
396489
+ });
396490
+ return { ok: true, driver: d, headless: wantHeadless };
396491
+ } catch {
396492
+ if (wantHeadless)
396493
+ return startFailure();
396494
+ try {
396495
+ clog("warning", `Failed to start context '${browserName}' on '${platform}'. Retrying as headless.`);
396496
+ const d = await driverStart(buildCaps(true), appiumPort, 4, {
396497
+ cacheDir: config?.cacheDir
396498
+ });
396499
+ return { ok: true, driver: d, headless: true };
396500
+ } catch {
396501
+ return startFailure();
396502
+ }
396503
+ }
396504
+ };
396505
+ let startedName;
396506
+ let startedHeadless = false;
396507
+ let lastError = "";
396508
+ for (const candidateName of candidateEngines) {
396509
+ const candidateCombo = combinationKey({
396510
+ platform: context.platform,
396511
+ browser: { name: candidateName }
396512
+ });
396513
+ if (warmUpDecision(warmUpResults.get(candidateCombo)) === "skip") {
396514
+ lastError = `context combination '${candidateName}' on '${platform}' could not start a driver earlier in this run.`;
396515
+ continue;
396516
+ }
396517
+ let res = await startDriverForBrowser(candidateName);
396518
+ if (!res.ok && shouldRepairBeforeFallback({
396519
+ candidateName,
396520
+ requestedName: requestedBrowserName,
396521
+ installAttempts
396522
+ })) {
396523
+ const outcome = await ensureContextBrowserInstalled({
396524
+ browserName: candidateName,
396525
+ config,
396526
+ installAttempts,
396527
+ deps: {
396528
+ ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
396529
+ log
396530
+ },
396531
+ repair: true
395952
396532
  });
395953
- driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
395954
- } catch (error2) {
395955
- let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
395956
- if (context.browser?.name === "safari" || context.browser?.name === "webkit")
395957
- errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
395958
- clog("error", errorMessage);
395959
- if (!warmUpResults.has(combo))
395960
- warmUpResults.set(combo, "failed");
395961
- contextReport.result = "SKIPPED";
395962
- contextReport.resultDescription = errorMessage;
395963
- return contextReport;
396533
+ if (outcome === "installed") {
396534
+ clog("info", `Repaired '${candidateName}' driver after a start failure; retrying before falling back.`);
396535
+ res = await startDriverForBrowser(candidateName);
396536
+ }
395964
396537
  }
396538
+ if (res.ok) {
396539
+ driver = res.driver;
396540
+ startedName = candidateName;
396541
+ startedHeadless = res.headless;
396542
+ if (!warmUpResults.has(candidateCombo))
396543
+ warmUpResults.set(candidateCombo, "ok");
396544
+ break;
396545
+ }
396546
+ clog("error", res.error);
396547
+ if (!warmUpResults.has(candidateCombo))
396548
+ warmUpResults.set(candidateCombo, "failed");
396549
+ lastError = res.error;
396550
+ }
396551
+ if (!startedName) {
396552
+ const errorMessage = driverSkipDiagnostic({
396553
+ requestedName: requestedBrowserName ?? "<none>",
396554
+ platform,
396555
+ platformMatches: true,
396556
+ attemptedFallback: candidateEngines.length > 1,
396557
+ lastError
396558
+ });
396559
+ clog("error", errorMessage);
396560
+ contextReport.result = "SKIPPED";
396561
+ contextReport.resultDescription = errorMessage;
396562
+ return contextReport;
396563
+ }
396564
+ if (startedHeadless) {
396565
+ context.browser = { ...context.browser, headless: true };
396566
+ contextReport.browser = context.browser;
396567
+ }
396568
+ if (normalizeBrowserName(startedName) !== normalizeBrowserName(requestedBrowserName)) {
396569
+ fellBackNote = `${requestedBrowserName} unavailable; ran on ${startedName}.`;
396570
+ fellBackPinned = explicitlyRequested;
396571
+ context.browser = { ...context.browser, name: startedName };
396572
+ contextReport.browser = context.browser;
396573
+ contextReport.fallback = {
396574
+ requested: requestedBrowserName,
396575
+ used: startedName
396576
+ };
396577
+ clog("warning", fellBackNote);
395965
396578
  }
395966
- if (!warmUpResults.has(combo))
395967
- warmUpResults.set(combo, "ok");
395968
396579
  if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
395969
396580
  await setViewportSize(context, driver);
395970
396581
  } else if (context.browser?.window?.width || context.browser?.window?.height) {
@@ -396214,6 +396825,12 @@ ${JSON.stringify(r, null, 2)}`);
396214
396825
  }
396215
396826
  }
396216
396827
  contextReport.result = rollUpResults(contextReport.steps);
396828
+ if (fellBackNote) {
396829
+ if (fellBackPinned && contextReport.result === "PASS") {
396830
+ contextReport.result = "WARNING";
396831
+ }
396832
+ contextReport.resultDescription = contextReport.resultDescription ? `${fellBackNote} ${contextReport.resultDescription}` : fellBackNote;
396833
+ }
396217
396834
  return contextReport;
396218
396835
  }
396219
396836
  async function stopAllRecordings({ config, context, driver, contextReport }) {
@@ -396381,9 +396998,9 @@ async function startAppiumServer(appiumEntry, config, display) {
396381
396998
  const port = await findFreePort();
396382
396999
  log(config, "debug", `Starting Appium on port ${port}`);
396383
397000
  const env = display ? { ...process.env, DISPLAY: display } : process.env;
396384
- const proc = (0, import_node_child_process7.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port)], {
397001
+ const proc = (0, import_node_child_process8.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port)], {
396385
397002
  windowsHide: true,
396386
- cwd: import_node_path20.default.join(__dirname5, "../.."),
397003
+ cwd: import_node_path21.default.join(__dirname5, "../.."),
396387
397004
  env
396388
397005
  });
396389
397006
  proc.on("error", (err) => {
@@ -396482,7 +397099,7 @@ async function ensureChromeAvailable(config, deps) {
396482
397099
  }
396483
397100
  return availableApps;
396484
397101
  }
396485
- async function ensureContextBrowserInstalled({ browserName, config, installAttempts, deps }) {
397102
+ async function ensureContextBrowserInstalled({ browserName, config, installAttempts, deps, repair = false }) {
396486
397103
  const key = (browserName ?? "<none>").toLowerCase();
396487
397104
  const cached = installAttempts.get(key);
396488
397105
  if (cached)
@@ -396495,9 +397112,13 @@ async function ensureContextBrowserInstalled({ browserName, config, installAttem
396495
397112
  const ctx = { cacheDir: config?.cacheDir };
396496
397113
  const logger = (msg, level = "info") => deps.log?.(config, level === "warn" ? "warning" : level, msg);
396497
397114
  try {
396498
- deps.log?.(config, "info", `Browser '${browserName}' is not available; attempting on-demand install of: ${assets.join(", ")}.`);
397115
+ deps.log?.(config, "info", `Browser '${browserName}' is not available; attempting on-demand ${repair ? "repair" : "install"} of: ${assets.join(", ")}.`);
396499
397116
  for (const asset of assets) {
396500
- await deps.ensureBrowser(asset, { ctx, deps: { logger } });
397117
+ await deps.ensureBrowser(asset, {
397118
+ ctx,
397119
+ deps: { logger },
397120
+ force: !!repair
397121
+ });
396501
397122
  }
396502
397123
  installAttempts.set(key, "installed");
396503
397124
  return "installed";
@@ -396528,9 +397149,9 @@ async function getRunner(options = {}) {
396528
397149
  if (!appiumEntry) {
396529
397150
  throw new Error("appium is not installed. Run `doc-detective install runtime appium` to install it.");
396530
397151
  }
396531
- const appium = (0, import_node_child_process7.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(appiumPort)], {
397152
+ const appium = (0, import_node_child_process8.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(appiumPort)], {
396532
397153
  windowsHide: true,
396533
- cwd: import_node_path20.default.join(__dirname5, "../..")
397154
+ cwd: import_node_path21.default.join(__dirname5, "../..")
396534
397155
  });
396535
397156
  appium.on("error", (err) => {
396536
397157
  log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);