doc-detective 4.28.0 → 4.29.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 (57) hide show
  1. package/dist/cli.js +13 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/common/src/detectTests.d.ts +9 -0
  4. package/dist/common/src/detectTests.d.ts.map +1 -1
  5. package/dist/common/src/detectTests.js +22 -9
  6. package/dist/common/src/detectTests.js.map +1 -1
  7. package/dist/common/src/schemas/schemas.json +85 -1
  8. package/dist/common/src/types/generated/report_v3.d.ts +34 -0
  9. package/dist/common/src/types/generated/report_v3.d.ts.map +1 -1
  10. package/dist/core/detectTests.d.ts.map +1 -1
  11. package/dist/core/detectTests.js +37 -13
  12. package/dist/core/detectTests.js.map +1 -1
  13. package/dist/core/expressions.d.ts.map +1 -1
  14. package/dist/core/expressions.js +24 -1
  15. package/dist/core/expressions.js.map +1 -1
  16. package/dist/core/index.d.ts +2 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +2 -2
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/resolveTests.js +23 -12
  21. package/dist/core/resolveTests.js.map +1 -1
  22. package/dist/core/telem.d.ts +2 -1
  23. package/dist/core/telem.d.ts.map +1 -1
  24. package/dist/core/telem.js +26 -2
  25. package/dist/core/telem.js.map +1 -1
  26. package/dist/core/tests/androidEmulator.d.ts.map +1 -1
  27. package/dist/core/tests/androidEmulator.js +13 -0
  28. package/dist/core/tests/androidEmulator.js.map +1 -1
  29. package/dist/core/tests/appSurface.d.ts +23 -0
  30. package/dist/core/tests/appSurface.d.ts.map +1 -1
  31. package/dist/core/tests/appSurface.js +3 -1
  32. package/dist/core/tests/appSurface.js.map +1 -1
  33. package/dist/core/tests/iosSimulator.d.ts.map +1 -1
  34. package/dist/core/tests/iosSimulator.js +13 -0
  35. package/dist/core/tests/iosSimulator.js.map +1 -1
  36. package/dist/core/tests/mobileBrowser.d.ts +1 -0
  37. package/dist/core/tests/mobileBrowser.d.ts.map +1 -1
  38. package/dist/core/tests/mobileBrowser.js +8 -0
  39. package/dist/core/tests/mobileBrowser.js.map +1 -1
  40. package/dist/core/tests.d.ts +83 -2
  41. package/dist/core/tests.d.ts.map +1 -1
  42. package/dist/core/tests.js +572 -70
  43. package/dist/core/tests.js.map +1 -1
  44. package/dist/core/utils.d.ts +3 -2
  45. package/dist/core/utils.d.ts.map +1 -1
  46. package/dist/core/utils.js +37 -29
  47. package/dist/core/utils.js.map +1 -1
  48. package/dist/core/warmPhase.d.ts +161 -0
  49. package/dist/core/warmPhase.d.ts.map +1 -0
  50. package/dist/core/warmPhase.js +341 -0
  51. package/dist/core/warmPhase.js.map +1 -0
  52. package/dist/index.cjs +776 -90
  53. package/dist/utils.d.ts +2 -1
  54. package/dist/utils.d.ts.map +1 -1
  55. package/dist/utils.js +11 -3
  56. package/dist/utils.js.map +1 -1
  57. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -383844,6 +383844,71 @@ var init_schemas = __esm({
383844
383844
  }
383845
383845
  ]
383846
383846
  }
383847
+ },
383848
+ warm: {
383849
+ type: "object",
383850
+ description: "Results of the run's inline warm phase \u2014 the always-on, best-effort provisioning pass (dependency installs, device boots, session probes) performed between test resolution and test execution. A failed task never gates the run; the per-context paths retry or skip with their normal semantics. Absent when the run ended before the phase (for example, when no specs matched). System-populated.",
383851
+ readOnly: true,
383852
+ additionalProperties: false,
383853
+ required: [
383854
+ "durationMs",
383855
+ "tasks"
383856
+ ],
383857
+ properties: {
383858
+ durationMs: {
383859
+ type: "number",
383860
+ description: "Wall-clock duration of the warm phase in milliseconds. Tasks overlap, so this is less than the sum of the per-task durations."
383861
+ },
383862
+ tasks: {
383863
+ type: "array",
383864
+ description: "One entry per executed warm task.",
383865
+ items: {
383866
+ type: "object",
383867
+ additionalProperties: false,
383868
+ required: [
383869
+ "name",
383870
+ "kind",
383871
+ "outcome",
383872
+ "durationMs"
383873
+ ],
383874
+ properties: {
383875
+ name: {
383876
+ type: "string",
383877
+ description: "Task identity, e.g. `browser-install:chrome` or `device-boot:android:<default>:<latest>`."
383878
+ },
383879
+ kind: {
383880
+ type: "string",
383881
+ enum: [
383882
+ "driver-install",
383883
+ "browser-install",
383884
+ "device-boot",
383885
+ "wda-check",
383886
+ "session-probe",
383887
+ "chromedriver-prefetch"
383888
+ ],
383889
+ description: "What the task pre-pays: a native app driver install, a browser install, a simulator/emulator boot, the managed WebDriverAgent availability check, the concurrent-run driver session probe, or the android mobile-web chromedriver download."
383890
+ },
383891
+ outcome: {
383892
+ type: "string",
383893
+ enum: [
383894
+ "warmed",
383895
+ "skipped",
383896
+ "failed"
383897
+ ],
383898
+ description: "`warmed` \u2014 the work was performed (for device boots, the boot was initiated); `skipped` \u2014 nothing to do or the environment isn't ready (the consuming context handles it as usual); `failed` \u2014 the attempt failed (logged as a warning; never fails the run)."
383899
+ },
383900
+ durationMs: {
383901
+ type: "number",
383902
+ description: "Wall-clock duration of this task in milliseconds."
383903
+ },
383904
+ note: {
383905
+ type: "string",
383906
+ description: "Human-readable detail about the outcome."
383907
+ }
383908
+ }
383909
+ }
383910
+ }
383911
+ }
383847
383912
  }
383848
383913
  },
383849
383914
  required: [
@@ -383865,7 +383930,26 @@ var init_schemas = __esm({
383865
383930
  }
383866
383931
  ]
383867
383932
  }
383868
- ]
383933
+ ],
383934
+ warm: {
383935
+ durationMs: 4211,
383936
+ tasks: [
383937
+ {
383938
+ name: "browser-install:chrome",
383939
+ kind: "browser-install",
383940
+ outcome: "skipped",
383941
+ durationMs: 2,
383942
+ note: "'chrome' is already available"
383943
+ },
383944
+ {
383945
+ name: "session-probe",
383946
+ kind: "session-probe",
383947
+ outcome: "warmed",
383948
+ durationMs: 4205,
383949
+ note: "1 combination ok"
383950
+ }
383951
+ ]
383952
+ }
383869
383953
  },
383870
383954
  {
383871
383955
  specs: [
@@ -633977,21 +634061,23 @@ async function loadEnvs(envsFile) {
633977
634061
  return { status: "FAIL", description: "Invalid file." };
633978
634062
  }
633979
634063
  }
634064
+ function logLevelEnabled(config, level) {
634065
+ if (config.logLevel === "error" && level === "error")
634066
+ return true;
634067
+ if (config.logLevel === "warning" && (level === "error" || level === "warning"))
634068
+ return true;
634069
+ if (config.logLevel === "info" && (level === "error" || level === "warning" || level === "info"))
634070
+ return true;
634071
+ if (config.logLevel === "debug" && (level === "error" || level === "warning" || level === "info" || level === "debug"))
634072
+ return true;
634073
+ return false;
634074
+ }
633980
634075
  async function log(config, level, message) {
633981
634076
  if (message === void 0) {
633982
634077
  message = config;
633983
634078
  config = {};
633984
634079
  }
633985
- let logLevelMatch = false;
633986
- if (config.logLevel === "error" && level === "error") {
633987
- logLevelMatch = true;
633988
- } else if (config.logLevel === "warning" && (level === "error" || level === "warning")) {
633989
- logLevelMatch = true;
633990
- } else if (config.logLevel === "info" && (level === "error" || level === "warning" || level === "info")) {
633991
- logLevelMatch = true;
633992
- } else if (config.logLevel === "debug" && (level === "error" || level === "warning" || level === "info" || level === "debug")) {
633993
- logLevelMatch = true;
633994
- }
634080
+ const logLevelMatch = logLevelEnabled(config, level);
633995
634081
  if (logLevelMatch) {
633996
634082
  if (typeof message === "string") {
633997
634083
  let logMessage = `(${level.toUpperCase()}) ${message}`;
@@ -634076,8 +634162,7 @@ function replaceEnvs(stringOrObject) {
634076
634162
  stringOrObject[key] = replaceEnvs(stringOrObject[key]);
634077
634163
  });
634078
634164
  } else if (typeof stringOrObject === "string") {
634079
- const variableRegex = new RegExp(/\$[a-zA-Z0-9_]+(?![a-zA-Z0-9_$])/, "g");
634080
- const matches = stringOrObject.match(variableRegex);
634165
+ const matches = stringOrObject.match(ENV_VAR_REGEX);
634081
634166
  if (!matches)
634082
634167
  return stringOrObject;
634083
634168
  matches.forEach((match) => {
@@ -634321,7 +634406,7 @@ function llevenshteinDistance(s, t) {
634321
634406
  }
634322
634407
  return arr[t.length][s.length];
634323
634408
  }
634324
- var import_node_fs7, import_node_os2, import_node_path5, import_node_crypto2, import_promises, import_node_net, import_axios, import_node_child_process4, BACKGROUND_BUFFER_LIMIT, PTY_PACKAGE, READY_POLL_INTERVAL_MS, TRANSIENT_SESSION_ERROR, SESSION_TIMEOUT_ABORT, TRANSIENT_PROCESS_INIT_EXIT_CODES, SETTLE_CEILING_MAX_MS, PRESERVED_TEMP_ENTRIES, FETCH_BINARY_DEFAULTS, posixBashProbe;
634409
+ var import_node_fs7, import_node_os2, import_node_path5, import_node_crypto2, import_promises, import_node_net, import_axios, import_node_child_process4, BACKGROUND_BUFFER_LIMIT, PTY_PACKAGE, READY_POLL_INTERVAL_MS, TRANSIENT_SESSION_ERROR, SESSION_TIMEOUT_ABORT, TRANSIENT_PROCESS_INIT_EXIT_CODES, SETTLE_CEILING_MAX_MS, PRESERVED_TEMP_ENTRIES, FETCH_BINARY_DEFAULTS, ENV_VAR_REGEX, posixBashProbe;
634325
634410
  var init_utils = __esm({
634326
634411
  "dist/core/utils.js"() {
634327
634412
  "use strict";
@@ -634363,6 +634448,7 @@ var init_utils = __esm({
634363
634448
  maxBodyLength: 50 * 1024 * 1024,
634364
634449
  maxRedirects: 5
634365
634450
  };
634451
+ ENV_VAR_REGEX = /\$[a-zA-Z0-9_]+(?![a-zA-Z0-9_$])/g;
634366
634452
  }
634367
634453
  });
634368
634454
 
@@ -636496,17 +636582,24 @@ function detectFileTypeFromContent(content) {
636496
636582
  }
636497
636583
 
636498
636584
  // dist/common/src/detectTests.js
636585
+ var compiledRegExpCache = /* @__PURE__ */ new Map();
636499
636586
  function safeRegExp(pattern, flags) {
636500
636587
  if (typeof pattern !== "string" || pattern.length === 0)
636501
636588
  return null;
636502
636589
  if (pattern.length > 1500)
636503
636590
  return null;
636504
- const sanitized = Array.from(pattern, (c) => String.fromCharCode(c.charCodeAt(0))).join("");
636591
+ const cacheKey = pattern + " " + flags;
636592
+ const cached = compiledRegExpCache.get(cacheKey);
636593
+ if (cached !== void 0)
636594
+ return cached;
636595
+ let compiled;
636505
636596
  try {
636506
- return new RegExp(sanitized, flags);
636597
+ compiled = new RegExp(pattern, flags);
636507
636598
  } catch {
636508
- return null;
636599
+ compiled = null;
636509
636600
  }
636601
+ compiledRegExpCache.set(cacheKey, compiled);
636602
+ return compiled;
636510
636603
  }
636511
636604
  var HASH_EXCLUDED_KEYS = /* @__PURE__ */ new Set([
636512
636605
  "location",
@@ -638219,12 +638312,16 @@ function generateSpecId(filePath) {
638219
638312
  const normalizedPath = relativePath.split(import_node_path11.default.sep).join("/").replace(/^\.\//, "").replace(/[^a-zA-Z0-9._\-\/]/g, "_");
638220
638313
  return normalizedPath;
638221
638314
  }
638222
- async function isValidSourceFile({ config, files, source }) {
638223
- log(config, "debug", `validation: ${source}`);
638224
- let allowedExtensions = ["json", "yaml", "yml"];
638225
- config.fileTypes.forEach((fileType) => {
638226
- allowedExtensions = allowedExtensions.concat(fileType.extensions);
638315
+ function buildAllowedExtensions(config) {
638316
+ const allowed = /* @__PURE__ */ new Set(["json", "yaml", "yml"]);
638317
+ (config.fileTypes || []).forEach((fileType) => {
638318
+ fileType.extensions.forEach((ext) => allowed.add(ext));
638227
638319
  });
638320
+ return allowed;
638321
+ }
638322
+ async function isValidSourceFile({ config, files, source, allowedExtensions }) {
638323
+ log(config, "debug", `validation: ${source}`);
638324
+ const allowed = allowedExtensions;
638228
638325
  if (files.indexOf(source) >= 0)
638229
638326
  return false;
638230
638327
  if (import_node_path11.default.extname(source) === ".json" || import_node_path11.default.extname(source) === ".yaml" || import_node_path11.default.extname(source) === ".yml") {
@@ -638271,7 +638368,7 @@ async function isValidSourceFile({ config, files, source }) {
638271
638368
  }
638272
638369
  }
638273
638370
  const extension = import_node_path11.default.extname(source).substring(1);
638274
- if (!allowedExtensions.includes(extension)) {
638371
+ if (!allowed.has(extension)) {
638275
638372
  log(config, "debug", `${source} extension isn't specified in a \`config.fileTypes\` object. Skipping.`);
638276
638373
  return false;
638277
638374
  }
@@ -638323,6 +638420,7 @@ async function qualifyFiles({ config }) {
638323
638420
  return [];
638324
638421
  }
638325
638422
  const ignoredDitaMaps = [];
638423
+ const allowedExtensions = buildAllowedExtensions(config);
638326
638424
  if (!config._herettoPathMapping) {
638327
638425
  config._herettoPathMapping = {};
638328
638426
  }
@@ -638390,8 +638488,9 @@ async function qualifyFiles({ config }) {
638390
638488
  let isFile = false;
638391
638489
  let isDir = false;
638392
638490
  try {
638393
- isFile = import_node_fs13.default.statSync(source).isFile();
638394
- isDir = import_node_fs13.default.statSync(source).isDirectory();
638491
+ const stat = import_node_fs13.default.statSync(source);
638492
+ isFile = stat.isFile();
638493
+ isDir = stat.isDirectory();
638395
638494
  } catch {
638396
638495
  log(config, "warning", `Cannot access path: ${source}. Skipping.`);
638397
638496
  continue;
@@ -638406,7 +638505,7 @@ async function qualifyFiles({ config }) {
638406
638505
  continue;
638407
638506
  }
638408
638507
  const resolved = import_node_path11.default.resolve(source);
638409
- if (isFile && await isValidSourceFile({ config, files, source: resolved })) {
638508
+ if (isFile && await isValidSourceFile({ config, files, allowedExtensions, source: resolved })) {
638410
638509
  files.push(resolved);
638411
638510
  if (!phaseByFile.has(resolved))
638412
638511
  phaseByFile.set(resolved, phase);
@@ -638419,9 +638518,10 @@ async function qualifyFiles({ config }) {
638419
638518
  const content = import_node_path11.default.resolve(dir + "/" + object);
638420
638519
  if (content.includes("node_modules"))
638421
638520
  continue;
638422
- const isFile2 = import_node_fs13.default.statSync(content).isFile();
638423
- const isDir2 = import_node_fs13.default.statSync(content).isDirectory();
638424
- if (isFile2 && await isValidSourceFile({ config, files, source: content })) {
638521
+ const stat = import_node_fs13.default.statSync(content);
638522
+ const isFile2 = stat.isFile();
638523
+ const isDir2 = stat.isDirectory();
638524
+ if (isFile2 && await isValidSourceFile({ config, files, allowedExtensions, source: content })) {
638425
638525
  const resolved2 = import_node_path11.default.resolve(content);
638426
638526
  files.push(resolved2);
638427
638527
  if (!phaseByFile.has(resolved2))
@@ -638785,12 +638885,14 @@ function resolveContexts({ contexts, test, config }) {
638785
638885
  if (resolvedContexts.length === 0) {
638786
638886
  resolvedContexts.push({});
638787
638887
  }
638788
- log(config, "debug", `Resolved contexts for test ${test.testId}:
638888
+ if (logLevelEnabled(config, "debug"))
638889
+ log(config, "debug", `Resolved contexts for test ${test.testId}:
638789
638890
  ${JSON.stringify(resolvedContexts, null, 2)}`);
638790
638891
  return resolvedContexts;
638791
638892
  }
638792
638893
  async function fetchOpenApiDocuments({ config, documentArray }) {
638793
- log(config, "debug", `Fetching OpenAPI documents:
638894
+ if (logLevelEnabled(config, "debug"))
638895
+ log(config, "debug", `Fetching OpenAPI documents:
638794
638896
  ${JSON.stringify(documentArray, null, 2)}`);
638795
638897
  const openApiDocuments = [];
638796
638898
  if (config?.integrations?.openApi?.length > 0)
@@ -638811,7 +638913,8 @@ ${JSON.stringify(documentArray, null, 2)}`);
638811
638913
  openApiDocuments.push(definition);
638812
638914
  }
638813
638915
  }
638814
- log(config, "debug", `Fetched OpenAPI documents:
638916
+ if (logLevelEnabled(config, "debug"))
638917
+ log(config, "debug", `Fetched OpenAPI documents:
638815
638918
  ${JSON.stringify(openApiDocuments, null, 2)}`);
638816
638919
  return openApiDocuments;
638817
638920
  }
@@ -638831,7 +638934,8 @@ async function resolveContext({ config, test, context, usedContextIds, openApi }
638831
638934
  context.contextId = context.contextId ? uniqueId(context.contextId, usedContextIds) : deriveContextId({ context, usedIds: usedContextIds });
638832
638935
  const contextId = context.contextId;
638833
638936
  usedContextIds.add(contextId);
638834
- log(config, "debug", `RESOLVING CONTEXT ID ${contextId}:
638937
+ if (logLevelEnabled(config, "debug"))
638938
+ log(config, "debug", `RESOLVING CONTEXT ID ${contextId}:
638835
638939
  ${JSON.stringify(context, null, 2)}`);
638836
638940
  const resolvedContext = {
638837
638941
  ...context,
@@ -638844,13 +638948,15 @@ ${JSON.stringify(context, null, 2)}`);
638844
638948
  steps: [...test.steps],
638845
638949
  contextId
638846
638950
  };
638847
- log(config, "debug", `RESOLVED CONTEXT ${contextId}:
638951
+ if (logLevelEnabled(config, "debug"))
638952
+ log(config, "debug", `RESOLVED CONTEXT ${contextId}:
638848
638953
  ${JSON.stringify(resolvedContext, null, 2)}`);
638849
638954
  return resolvedContext;
638850
638955
  }
638851
638956
  async function resolveTest({ config, spec, test }) {
638852
638957
  const testId = test.testId || `${spec.specId}~${contentHash(test)}`;
638853
- log(config, "debug", `RESOLVING TEST ID ${testId}:
638958
+ if (logLevelEnabled(config, "debug"))
638959
+ log(config, "debug", `RESOLVING TEST ID ${testId}:
638854
638960
  ${JSON.stringify(test, null, 2)}`);
638855
638961
  const resolvedTest = {
638856
638962
  ...test,
@@ -638880,13 +638986,15 @@ ${JSON.stringify(test, null, 2)}`);
638880
638986
  });
638881
638987
  resolvedTest.contexts.push(resolvedContext);
638882
638988
  }
638883
- log(config, "debug", `RESOLVED TEST ${testId}:
638989
+ if (logLevelEnabled(config, "debug"))
638990
+ log(config, "debug", `RESOLVED TEST ${testId}:
638884
638991
  ${JSON.stringify(resolvedTest, null, 2)}`);
638885
638992
  return resolvedTest;
638886
638993
  }
638887
638994
  async function resolveSpec({ config, spec }) {
638888
638995
  const specId = spec.specId || (spec.contentPath ? generateSpecId(spec.contentPath) : import_node_crypto5.default.randomUUID());
638889
- log(config, "debug", `RESOLVING SPEC ID ${specId}:
638996
+ if (logLevelEnabled(config, "debug"))
638997
+ log(config, "debug", `RESOLVING SPEC ID ${specId}:
638890
638998
  ${JSON.stringify(spec, null, 2)}`);
638891
638999
  const resolvedSpec = {
638892
639000
  ...spec,
@@ -638906,12 +639014,14 @@ ${JSON.stringify(spec, null, 2)}`);
638906
639014
  });
638907
639015
  resolvedSpec.tests.push(resolvedTest);
638908
639016
  }
638909
- log(config, "debug", `RESOLVED SPEC ${specId}:
639017
+ if (logLevelEnabled(config, "debug"))
639018
+ log(config, "debug", `RESOLVED SPEC ${specId}:
638910
639019
  ${JSON.stringify(resolvedSpec, null, 2)}`);
638911
639020
  return resolvedSpec;
638912
639021
  }
638913
639022
  async function resolveTests({ config, detectedTests }) {
638914
- log(config, "debug", `RESOLVING DETECTED TEST SPECS:
639023
+ if (logLevelEnabled(config, "debug"))
639024
+ log(config, "debug", `RESOLVING DETECTED TEST SPECS:
638915
639025
  ${JSON.stringify(detectedTests, null, 2)}`);
638916
639026
  const resolvedTests = {
638917
639027
  resolvedTestsId: import_node_crypto5.default.randomUUID(),
@@ -638923,7 +639033,8 @@ ${JSON.stringify(detectedTests, null, 2)}`);
638923
639033
  const resolvedSpec = await resolveSpec({ config, spec });
638924
639034
  resolvedTests.specs.push(resolvedSpec);
638925
639035
  }
638926
- log(config, "debug", `RESOLVED TEST SPECS:
639036
+ if (logLevelEnabled(config, "debug"))
639037
+ log(config, "debug", `RESOLVED TEST SPECS:
638927
639038
  ${JSON.stringify(resolvedTests, null, 2)}`);
638928
639039
  return resolvedTests;
638929
639040
  }
@@ -641084,6 +641195,13 @@ async function teardownDeviceRegistry(registry, kill4) {
641084
641195
  for (const entry of registry.values()) {
641085
641196
  if (!entry.bootedByUs)
641086
641197
  continue;
641198
+ if (entry.ready) {
641199
+ try {
641200
+ await entry.ready;
641201
+ } catch {
641202
+ continue;
641203
+ }
641204
+ }
641087
641205
  try {
641088
641206
  await kill4(entry);
641089
641207
  } catch {
@@ -643777,6 +643895,7 @@ function containsOperators(expression, allowOperators = false) {
643777
643895
  }
643778
643896
  return false;
643779
643897
  }
643898
+ var compiledEvaluatorCache = /* @__PURE__ */ new Map();
643780
643899
  async function evaluateExpression(expression, context) {
643781
643900
  try {
643782
643901
  expression = preprocessExpression(expression);
@@ -643850,7 +643969,13 @@ async function evaluateExpression(expression, context) {
643850
643969
  }
643851
643970
  })
643852
643971
  };
643853
- const evaluator = new Function(...Object.keys(evalContext), `return ${expression};`);
643972
+ const argNames = Object.keys(evalContext);
643973
+ const cacheKey = `${argNames.join(",")}\0${expression}`;
643974
+ let evaluator = compiledEvaluatorCache.get(cacheKey);
643975
+ if (!evaluator) {
643976
+ evaluator = new Function(...argNames, `return ${expression};`);
643977
+ compiledEvaluatorCache.set(cacheKey, evaluator);
643978
+ }
643854
643979
  return evaluator(...Object.values(evalContext));
643855
643980
  } catch (error) {
643856
643981
  log(`Error evaluating expression '${expression}': ${error.message}`, "error");
@@ -648672,6 +648797,10 @@ function mobileBrowserConfigError(browser) {
648672
648797
  }
648673
648798
  return null;
648674
648799
  }
648800
+ var CHROMEDRIVER_AUTODOWNLOAD_ARGS = [
648801
+ "--allow-insecure",
648802
+ "uiautomator2:chromedriver_autodownload"
648803
+ ];
648675
648804
  function mobileBrowserGate({ platform, browser, hasBrowserStep, hasAppStep }) {
648676
648805
  if (!hasBrowserStep)
648677
648806
  return { action: "proceed", browserName: null };
@@ -648732,6 +648861,216 @@ function buildMobileBrowserCapabilities({ platform, udid, cacheDir, timeout, loc
648732
648861
  return capabilities;
648733
648862
  }
648734
648863
 
648864
+ // dist/core/warmPhase.js
648865
+ init_utils();
648866
+ var WARM_POOL_LIMIT = 4;
648867
+ var RUNTIME_INSTALL_RESOURCE = "runtime-install";
648868
+ function deviceIdentity(platform, desc) {
648869
+ if (desc?.name)
648870
+ return `${platform}:name:${desc.name}`;
648871
+ return `${platform}:default:${desc?.deviceType ?? "<any>"}:${desc?.osVersion ?? "<latest>"}`;
648872
+ }
648873
+ function deviceResourceTag(platform, desc) {
648874
+ return `warm-device:${deviceIdentity(platform, desc)}`;
648875
+ }
648876
+ function wrapInitiationEffects(deps, keys, signal) {
648877
+ const wrapped = { ...deps };
648878
+ for (const key of keys) {
648879
+ const original = deps[key];
648880
+ if (typeof original !== "function")
648881
+ continue;
648882
+ wrapped[key] = (...args) => {
648883
+ const result = original(...args);
648884
+ signal();
648885
+ return result;
648886
+ };
648887
+ }
648888
+ return wrapped;
648889
+ }
648890
+ function planWarmTasks({ sizingJobs, runnerDetails, limit, hasAppiumPool, deps }) {
648891
+ const hostPlatform = runnerDetails?.environment?.platform;
648892
+ const tasks = [];
648893
+ const seen = /* @__PURE__ */ new Set();
648894
+ const addTask = (task) => {
648895
+ if (seen.has(task.name))
648896
+ return;
648897
+ seen.add(task.name);
648898
+ tasks.push(task);
648899
+ };
648900
+ let probeEligible = false;
648901
+ let defaultBrowser;
648902
+ const getDefaultBrowser2 = () => defaultBrowser ??= deps.getDefaultBrowser({ runnerDetails });
648903
+ const bootPlannedFor = /* @__PURE__ */ new Set();
648904
+ for (const job of sizingJobs ?? []) {
648905
+ const context = job?.context;
648906
+ if (!context)
648907
+ continue;
648908
+ if (deps.contextRequirementsSkipMessage({ context }))
648909
+ continue;
648910
+ const effPlatform = context.platform || hostPlatform;
648911
+ const mobileTarget = deps.isMobileTargetPlatform(effPlatform);
648912
+ if (mobileTarget) {
648913
+ if (mobileTarget === "ios" && hostPlatform !== "mac")
648914
+ continue;
648915
+ const hasAppStep = deps.isAppDriverRequired({ test: context });
648916
+ const gate = deps.mobileBrowserGate({
648917
+ platform: mobileTarget,
648918
+ browser: context.browser,
648919
+ hasBrowserStep: deps.isBrowserRequired({ test: context }),
648920
+ hasAppStep
648921
+ });
648922
+ if (gate.action !== "proceed")
648923
+ continue;
648924
+ const isMobileWeb = typeof gate.browserName === "string";
648925
+ if (!isMobileWeb && !hasAppStep)
648926
+ continue;
648927
+ const driverPackage = deps.appDriverPlatforms[mobileTarget]?.driverPackage;
648928
+ if (driverPackage) {
648929
+ addTask({
648930
+ name: `driver-install:${driverPackage}`,
648931
+ kind: "driver-install",
648932
+ exclusiveResources: [RUNTIME_INSTALL_RESOURCE],
648933
+ payload: { driverPackage, platform: mobileTarget }
648934
+ });
648935
+ }
648936
+ for (const stepDevice of deps.collectDeviceDescriptors(context)) {
648937
+ const desc = deps.normalizeDeviceDescriptor({
648938
+ contextDevice: context.device,
648939
+ stepDevice,
648940
+ platform: mobileTarget
648941
+ });
648942
+ const identity = deviceIdentity(mobileTarget, desc);
648943
+ const tag = deviceResourceTag(mobileTarget, desc);
648944
+ if (!bootPlannedFor.has(mobileTarget)) {
648945
+ bootPlannedFor.add(mobileTarget);
648946
+ addTask({
648947
+ name: `device-boot:${identity}`,
648948
+ kind: "device-boot",
648949
+ exclusiveResources: [tag],
648950
+ payload: { platform: mobileTarget, desc }
648951
+ });
648952
+ }
648953
+ if (mobileTarget === "android" && isMobileWeb) {
648954
+ addTask({
648955
+ name: `chromedriver-prefetch:${identity}`,
648956
+ kind: "chromedriver-prefetch",
648957
+ exclusiveResources: [tag],
648958
+ payload: { platform: mobileTarget, desc }
648959
+ });
648960
+ }
648961
+ }
648962
+ if (mobileTarget === "ios") {
648963
+ addTask({
648964
+ name: "wda-check",
648965
+ kind: "wda-check",
648966
+ // Read-only (plus the last-used stamp) — contends with nothing.
648967
+ exclusiveResources: [],
648968
+ payload: {}
648969
+ });
648970
+ }
648971
+ continue;
648972
+ }
648973
+ if (deps.isBrowserRequired({ test: context })) {
648974
+ const effBrowser = context.browser ?? getDefaultBrowser2();
648975
+ const browserName = effBrowser?.name;
648976
+ if (browserName && effPlatform === hostPlatform) {
648977
+ probeEligible = true;
648978
+ if (deps.requiredBrowserAssets(browserName).length > 0) {
648979
+ addTask({
648980
+ name: `browser-install:${browserName.toLowerCase()}`,
648981
+ kind: "browser-install",
648982
+ exclusiveResources: [RUNTIME_INSTALL_RESOURCE],
648983
+ payload: { browserName }
648984
+ });
648985
+ }
648986
+ }
648987
+ }
648988
+ if ((effPlatform === "windows" || effPlatform === "mac") && effPlatform === hostPlatform && deps.isAppDriverRequired({ test: context })) {
648989
+ const driverPackage = deps.appDriverPlatforms[effPlatform]?.driverPackage;
648990
+ if (driverPackage) {
648991
+ addTask({
648992
+ name: `driver-install:${driverPackage}`,
648993
+ kind: "driver-install",
648994
+ exclusiveResources: [RUNTIME_INSTALL_RESOURCE],
648995
+ payload: { driverPackage, platform: effPlatform }
648996
+ });
648997
+ }
648998
+ }
648999
+ }
649000
+ if (limit > 1 && hasAppiumPool && probeEligible) {
649001
+ addTask({
649002
+ name: "session-probe",
649003
+ kind: "session-probe",
649004
+ // Its install half mutates the shared caches, exactly like the
649005
+ // dedicated install tasks (usually a memo hit by the time it runs).
649006
+ exclusiveResources: [RUNTIME_INSTALL_RESOURCE],
649007
+ payload: {}
649008
+ });
649009
+ }
649010
+ return tasks;
649011
+ }
649012
+ async function executeWarmTasks({ tasks, registry, runTask, log: log3, now = Date.now }) {
649013
+ const results = [];
649014
+ const start = now();
649015
+ await runResourceAware(tasks, WARM_POOL_LIMIT, registry, async (task) => {
649016
+ const taskStart = now();
649017
+ try {
649018
+ const { outcome, note } = await runTask(task);
649019
+ results.push({
649020
+ name: task.name,
649021
+ kind: task.kind,
649022
+ outcome,
649023
+ durationMs: now() - taskStart,
649024
+ ...note ? { note } : {}
649025
+ });
649026
+ if (outcome === "failed") {
649027
+ log3("warning", `Warm task '${task.name}' failed (continuing)${note ? `: ${note}` : "."}`);
649028
+ } else {
649029
+ log3("debug", `Warm task '${task.name}': ${outcome}${note ? ` (${note})` : ""}.`);
649030
+ }
649031
+ } catch (error) {
649032
+ const note = error?.message ?? String(error);
649033
+ results.push({
649034
+ name: task.name,
649035
+ kind: task.kind,
649036
+ outcome: "failed",
649037
+ durationMs: now() - taskStart,
649038
+ note
649039
+ });
649040
+ log3("warning", `Warm task '${task.name}' failed (continuing): ${note}`);
649041
+ }
649042
+ });
649043
+ return { durationMs: now() - start, tasks: results };
649044
+ }
649045
+ function raceBootInitiation({ startAcquire, onError }) {
649046
+ let initiatedResolve;
649047
+ const initiated = new Promise((resolve) => {
649048
+ initiatedResolve = resolve;
649049
+ });
649050
+ const acquired = (async () => {
649051
+ try {
649052
+ const result = await startAcquire(() => initiatedResolve());
649053
+ if (result && typeof result === "object" && "skip" in result) {
649054
+ return { outcome: "skipped", note: result.skip };
649055
+ }
649056
+ return { outcome: "warmed", note: "device ready" };
649057
+ } catch (error) {
649058
+ onError(error);
649059
+ return {
649060
+ outcome: "failed",
649061
+ note: error?.message ?? String(error)
649062
+ };
649063
+ }
649064
+ })();
649065
+ return Promise.race([
649066
+ initiated.then(() => ({
649067
+ outcome: "warmed",
649068
+ note: "boot initiated"
649069
+ })),
649070
+ acquired
649071
+ ]);
649072
+ }
649073
+
648735
649074
  // dist/core/tests.js
648736
649075
  init_cacheDir();
648737
649076
 
@@ -649001,6 +649340,13 @@ async function teardownSimulatorRegistry(registry, shutdown) {
649001
649340
  for (const entry of registry.values()) {
649002
649341
  if (!entry.bootedByUs)
649003
649342
  continue;
649343
+ if (entry.ready) {
649344
+ try {
649345
+ await entry.ready;
649346
+ } catch {
649347
+ continue;
649348
+ }
649349
+ }
649004
649350
  try {
649005
649351
  await shutdown(entry);
649006
649352
  } catch {
@@ -649537,6 +649883,20 @@ function combinationKey(context) {
649537
649883
  function warmUpDecision(prev) {
649538
649884
  return prev === "failed" ? "skip" : "attempt";
649539
649885
  }
649886
+ function buildWarmPlanDeps() {
649887
+ return {
649888
+ isBrowserRequired,
649889
+ isAppDriverRequired,
649890
+ isMobileTargetPlatform,
649891
+ getDefaultBrowser,
649892
+ requiredBrowserAssets,
649893
+ collectDeviceDescriptors,
649894
+ normalizeDeviceDescriptor,
649895
+ mobileBrowserGate,
649896
+ contextRequirementsSkipMessage,
649897
+ appDriverPlatforms: APP_DRIVER_PLATFORMS
649898
+ };
649899
+ }
649540
649900
  function getDriverCapabilities({ runnerDetails, name, options }) {
649541
649901
  let capabilities = {};
649542
649902
  let args = [];
@@ -650146,7 +650506,12 @@ async function runSpecs({ resolvedTests }) {
650146
650506
  skipped: 0
650147
650507
  }
650148
650508
  },
650149
- specs: []
650509
+ specs: [],
650510
+ // Inline warm phase results (docs/design/warm-phase.md). Present on the
650511
+ // skeleton so a run that plans nothing (or whose planning fails —
650512
+ // best-effort) still reports the structural empty block; the phase
650513
+ // overwrites it with real task results below.
650514
+ warm: { durationMs: 0, tasks: [] }
650150
650515
  };
650151
650516
  let limit = resolveConcurrentRunners(config);
650152
650517
  log(config, "info", "Running test specs.");
@@ -650301,6 +650666,7 @@ async function runSpecs({ resolvedTests }) {
650301
650666
  const serverCount = Math.min(limit, browserPoolJobCount);
650302
650667
  log(config, "debug", `Starting ${serverCount} Appium server(s).`);
650303
650668
  try {
650669
+ const readinessWaits = [];
650304
650670
  for (let i = 0; i < serverCount; i++) {
650305
650671
  let display;
650306
650672
  if (useXvfbDisplays) {
@@ -650308,7 +650674,16 @@ async function runSpecs({ resolvedTests }) {
650308
650674
  xvfbProcesses.push(await startXvfb(display));
650309
650675
  log(config, "debug", `Started Xvfb on ${display} for recording.`);
650310
650676
  }
650311
- appiumServers.push(await startAppiumServer(appiumEntry, config, display));
650677
+ const server = await spawnAppiumServer(appiumEntry, config, display);
650678
+ appiumServers.push(server);
650679
+ const wait2 = appiumIsReady(server.port);
650680
+ wait2.catch(() => {
650681
+ });
650682
+ readinessWaits.push(wait2);
650683
+ }
650684
+ await Promise.all(readinessWaits);
650685
+ for (const server of appiumServers) {
650686
+ log(config, "debug", `Appium is ready on port ${server.port}.`);
650312
650687
  }
650313
650688
  } catch (error) {
650314
650689
  await Promise.all(appiumServers.map((server) => {
@@ -650331,6 +650706,7 @@ async function runSpecs({ resolvedTests }) {
650331
650706
  const processRegistry = /* @__PURE__ */ new Map();
650332
650707
  const deviceRegistry = createDeviceRegistry();
650333
650708
  const simulatorRegistry = createSimulatorRegistry();
650709
+ const resourceRegistry = createResourceRegistry();
650334
650710
  const killAllRegistered = async () => {
650335
650711
  const entries = [...processRegistry.entries()];
650336
650712
  processRegistry.clear();
@@ -650367,15 +650743,35 @@ async function runSpecs({ resolvedTests }) {
650367
650743
  process.on("SIGINT", onSignal);
650368
650744
  process.on("SIGTERM", onSignal);
650369
650745
  try {
650370
- if (limit > 1 && appiumPool) {
650371
- await warmUpContexts({
650372
- jobs: sizingJobs,
650373
- config,
650746
+ try {
650747
+ const warmTasks = planWarmTasks({
650748
+ sizingJobs,
650374
650749
  runnerDetails,
650375
- appiumPool,
650376
- installAttempts,
650377
- warmUpResults
650750
+ limit,
650751
+ hasAppiumPool: !!appiumPool,
650752
+ deps: buildWarmPlanDeps()
650378
650753
  });
650754
+ if (warmTasks.length > 0) {
650755
+ log(config, "debug", `Warm phase: ${warmTasks.length} task(s).`);
650756
+ report.warm = await executeWarmTasks({
650757
+ tasks: warmTasks,
650758
+ registry: resourceRegistry,
650759
+ runTask: buildWarmTaskRunner({
650760
+ config,
650761
+ runnerDetails,
650762
+ sizingJobs,
650763
+ appiumPool,
650764
+ installAttempts,
650765
+ warmUpResults,
650766
+ deviceRegistry,
650767
+ simulatorRegistry,
650768
+ resourceRegistry
650769
+ }),
650770
+ log: (level, message) => log(config, level, message)
650771
+ });
650772
+ }
650773
+ } catch (error) {
650774
+ log(config, "warning", `Warm phase skipped (planning failed; the run proceeds with on-demand provisioning): ${error?.message ?? error}`);
650379
650775
  }
650380
650776
  const runJob = async (job) => {
650381
650777
  try {
@@ -650427,7 +650823,6 @@ async function runSpecs({ resolvedTests }) {
650427
650823
  const phase = PHASES.includes(entry.spec?._phase) ? entry.spec._phase : "main";
650428
650824
  routedByPhase[phase].push(entry);
650429
650825
  }
650430
- const resourceRegistry = createResourceRegistry();
650431
650826
  for (const phase of PHASES) {
650432
650827
  if (limit > 1) {
650433
650828
  await runResourceAware(jobsByPhase[phase], limit, resourceRegistry, runJob);
@@ -650741,27 +651136,17 @@ async function warmUpContexts({ jobs, config, runnerDetails, appiumPool, install
650741
651136
  platform
650742
651137
  });
650743
651138
  if (!supported && context.platform === platform && Array.isArray(context?.steps) && requiredBrowserAssets(context.browser?.name).length > 0) {
650744
- const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
650745
- const outcome = await ensureContextBrowserInstalled({
651139
+ await warmBrowserInstall({
650746
651140
  browserName: context.browser?.name,
650747
651141
  config,
650748
- installAttempts,
650749
- deps: {
650750
- ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
650751
- log
650752
- },
650753
- // Repair a present-but-broken driver, not just install-if-missing.
650754
- repair: true
651142
+ runnerDetails,
651143
+ installAttempts
651144
+ });
651145
+ supported = isSupportedContext({
651146
+ context,
651147
+ apps: runnerDetails.availableApps,
651148
+ platform
650755
651149
  });
650756
- if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
650757
- clearAppCache(config);
650758
- runnerDetails.availableApps = await getAvailableApps({ config });
650759
- supported = isSupportedContext({
650760
- context,
650761
- apps: runnerDetails.availableApps,
650762
- platform
650763
- });
650764
- }
650765
651150
  }
650766
651151
  if (!supported)
650767
651152
  continue;
@@ -650803,6 +651188,292 @@ async function warmUpContexts({ jobs, config, runnerDetails, appiumPool, install
650803
651188
  }
650804
651189
  }
650805
651190
  }
651191
+ async function warmBrowserInstall({ browserName, config, runnerDetails, installAttempts, deps = {} }) {
651192
+ const appName = normalizeBrowserName(browserName);
651193
+ if (runnerDetails.availableApps?.find((app) => app.name === appName)) {
651194
+ return {
651195
+ outcome: "skipped",
651196
+ note: `'${browserName}' is already available`
651197
+ };
651198
+ }
651199
+ const firstAttempt = !installAttempts.has((browserName ?? "<none>").toLowerCase());
651200
+ const outcome = await ensureContextBrowserInstalled({
651201
+ browserName,
651202
+ config,
651203
+ installAttempts,
651204
+ deps: {
651205
+ ensureBrowser: deps.ensureBrowser ?? ((asset, options) => ensureBrowserInstalled(asset, options)),
651206
+ log
651207
+ },
651208
+ // Repair a present-but-broken driver, not just install-if-missing.
651209
+ repair: true
651210
+ });
651211
+ if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
651212
+ (deps.clearAppCache ?? clearAppCache)(config);
651213
+ runnerDetails.availableApps = await (deps.getAvailableApps ?? getAvailableApps)({ config });
651214
+ }
651215
+ if (outcome === "installed")
651216
+ return { outcome: "warmed" };
651217
+ if (outcome === "failed") {
651218
+ return { outcome: "failed", note: `couldn't install '${browserName}'` };
651219
+ }
651220
+ return {
651221
+ outcome: "skipped",
651222
+ note: `'${browserName}' has no installable assets`
651223
+ };
651224
+ }
651225
+ async function resolveAndroidWarmEnv(config) {
651226
+ try {
651227
+ const abi = hostAbi();
651228
+ const sdk = detectAndroidSdk({ cacheDir: config?.cacheDir });
651229
+ if (!sdk?.emulator)
651230
+ return null;
651231
+ const deviceDeps = buildAcquireDeviceDeps(sdk, abi, (m) => log(config, "debug", m));
651232
+ const running = await deviceDeps.listRunning();
651233
+ const capable = running.length > 0 || await checkEmulatorAcceleration(sdk.emulator);
651234
+ if (!capable)
651235
+ return null;
651236
+ return { sdkRoot: sdk.sdkRoot, deviceDeps };
651237
+ } catch {
651238
+ return null;
651239
+ }
651240
+ }
651241
+ async function probeIosToolchainWarm() {
651242
+ if (process.platform !== "darwin")
651243
+ return probeIosToolchain();
651244
+ const runAsync = (command, args, timeout) => new Promise((resolve) => {
651245
+ let stdout = "";
651246
+ let stderr = "";
651247
+ try {
651248
+ const child = (0, import_node_child_process15.spawn)(command, args, { windowsHide: true, timeout });
651249
+ child.stdout?.on("data", (d) => stdout += String(d));
651250
+ child.stderr?.on("data", (d) => stderr += String(d));
651251
+ child.on("error", () => resolve({ status: null, stdout, stderr }));
651252
+ child.on("close", (status) => resolve({ status, stdout, stderr }));
651253
+ } catch {
651254
+ resolve({ status: null, stdout, stderr });
651255
+ }
651256
+ });
651257
+ const [xcodeSelect, simctl] = await Promise.all([
651258
+ runAsync("xcode-select", ["-p"], 15e3),
651259
+ // Same generous ceiling as the sync probe's xcrun spawn: the first cold
651260
+ // simctl call launches CoreSimulatorService.
651261
+ runAsync("xcrun", ["simctl", "list", "devices", "available"], 12e4)
651262
+ ]);
651263
+ return probeIosToolchain({
651264
+ run: (command) => command === "xcrun" ? simctl : xcodeSelect
651265
+ });
651266
+ }
651267
+ function buildWarmTaskRunner({ config, runnerDetails, sizingJobs, appiumPool, installAttempts, warmUpResults, deviceRegistry, simulatorRegistry, resourceRegistry }) {
651268
+ let androidEnv;
651269
+ const getAndroidEnv = () => androidEnv ??= resolveAndroidWarmEnv(config);
651270
+ let iosToolchain;
651271
+ const getIosToolchain = () => iosToolchain ??= probeIosToolchainWarm();
651272
+ const acquireLease = async (names) => {
651273
+ while (!resourceRegistry.tryAcquire(names)) {
651274
+ await resourceRegistry.waitForFree();
651275
+ }
651276
+ let released = false;
651277
+ return () => {
651278
+ if (released)
651279
+ return;
651280
+ released = true;
651281
+ resourceRegistry.release(names);
651282
+ };
651283
+ };
651284
+ const withRuntimeInstallLock = async (fn) => {
651285
+ const release = await acquireLease([RUNTIME_INSTALL_RESOURCE]);
651286
+ try {
651287
+ return await fn();
651288
+ } finally {
651289
+ release();
651290
+ }
651291
+ };
651292
+ let androidPreflight;
651293
+ const getAndroidPreflight = () => androidPreflight ??= withRuntimeInstallLock(() => appSurfacePreflight({ config, platform: "android" }));
651294
+ return async (task) => {
651295
+ switch (task.kind) {
651296
+ case "browser-install":
651297
+ return warmBrowserInstall({
651298
+ browserName: task.payload.browserName,
651299
+ config,
651300
+ runnerDetails,
651301
+ installAttempts
651302
+ });
651303
+ case "driver-install": {
651304
+ if (task.payload.platform === "android") {
651305
+ const env = await getAndroidEnv();
651306
+ if (!env) {
651307
+ return {
651308
+ outcome: "skipped",
651309
+ note: "Android toolchain not ready; the driver install stays with the consuming context"
651310
+ };
651311
+ }
651312
+ }
651313
+ if (task.payload.platform === "ios") {
651314
+ const toolchain = await getIosToolchain();
651315
+ if (!toolchain.ok) {
651316
+ return { outcome: "skipped", note: toolchain.reason };
651317
+ }
651318
+ }
651319
+ const { ensureRuntimeInstalled: ensureRuntimeInstalled2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
651320
+ await ensureRuntimeInstalled2([task.payload.driverPackage], {
651321
+ ctx: { cacheDir: config?.cacheDir },
651322
+ deps: { logger: (m) => log(config, "debug", m) }
651323
+ });
651324
+ return { outcome: "warmed" };
651325
+ }
651326
+ case "device-boot": {
651327
+ const desc = task.payload.desc;
651328
+ const onError = (error) => log(config, "warning", `Warm boot of '${task.name}' failed (a consuming context will retry): ${error?.message ?? String(error)}`);
651329
+ if (task.payload.platform === "android") {
651330
+ const env = await getAndroidEnv();
651331
+ if (!env) {
651332
+ return {
651333
+ outcome: "skipped",
651334
+ note: "Android toolchain not ready; device setup stays with the consuming context"
651335
+ };
651336
+ }
651337
+ const releaseEmulatorLease = await acquireLease([
651338
+ "android-emulator"
651339
+ ]);
651340
+ return raceBootInitiation({
651341
+ onError,
651342
+ startAcquire: (signalInitiated) => {
651343
+ const acquiring = acquireDevice({
651344
+ desc,
651345
+ registry: deviceRegistry,
651346
+ sdkRoot: env.sdkRoot,
651347
+ deps: wrapInitiationEffects(env.deviceDeps, ["createAvd", "boot"], signalInitiated)
651348
+ });
651349
+ acquiring.then(() => releaseEmulatorLease(), () => releaseEmulatorLease());
651350
+ return acquiring;
651351
+ }
651352
+ });
651353
+ }
651354
+ const toolchain = await getIosToolchain();
651355
+ if (!toolchain.ok) {
651356
+ return { outcome: "skipped", note: toolchain.reason };
651357
+ }
651358
+ const simDeps = buildAcquireSimulatorDeps((m) => log(config, "debug", m));
651359
+ return raceBootInitiation({
651360
+ onError,
651361
+ startAcquire: (signalInitiated) => acquireSimulator({
651362
+ desc,
651363
+ registry: simulatorRegistry,
651364
+ deps: wrapInitiationEffects(simDeps, ["create", "boot"], signalInitiated)
651365
+ })
651366
+ });
651367
+ }
651368
+ case "wda-check": {
651369
+ const hit = locateManagedWda({ ctx: { cacheDir: config?.cacheDir } });
651370
+ if (hit) {
651371
+ return {
651372
+ outcome: "warmed",
651373
+ note: `prebuilt WebDriverAgent available (${hit.key})`
651374
+ };
651375
+ }
651376
+ return {
651377
+ outcome: "skipped",
651378
+ note: "no prebuilt WebDriverAgent for the current toolchain \u2014 `doc-detective install ios` prebuilds it"
651379
+ };
651380
+ }
651381
+ case "session-probe": {
651382
+ if (!appiumPool) {
651383
+ return { outcome: "skipped", note: "no browser Appium pool" };
651384
+ }
651385
+ await warmUpContexts({
651386
+ jobs: sizingJobs,
651387
+ config,
651388
+ runnerDetails,
651389
+ appiumPool,
651390
+ installAttempts,
651391
+ warmUpResults
651392
+ });
651393
+ const ok = [...warmUpResults.values()].filter((v) => v === "ok").length;
651394
+ const failed = warmUpResults.size - ok;
651395
+ return {
651396
+ outcome: "warmed",
651397
+ note: `${ok} combination${ok === 1 ? "" : "s"} ok${failed ? `, ${failed} failed` : ""}`
651398
+ };
651399
+ }
651400
+ case "chromedriver-prefetch":
651401
+ return prefetchMobileChromedriver({
651402
+ config,
651403
+ desc: task.payload.desc,
651404
+ deviceRegistry,
651405
+ getAndroidEnv,
651406
+ deps: {
651407
+ // The cache-mutating half (driver install + Appium co-homing)
651408
+ // runs once per run under the manual runtime-install lease, so
651409
+ // the prefetch task itself only holds its device tag while it
651410
+ // awaits readiness and runs the throwaway session.
651411
+ appSurfacePreflight: () => getAndroidPreflight(),
651412
+ acquireEmulatorLease: () => acquireLease(["android-emulator"])
651413
+ }
651414
+ });
651415
+ }
651416
+ };
651417
+ }
651418
+ async function prefetchMobileChromedriver({ config, desc, deviceRegistry, getAndroidEnv, deps = {} }) {
651419
+ const preflight = deps.appSurfacePreflight ?? appSurfacePreflight;
651420
+ const acquire = deps.acquireDevice ?? acquireDevice;
651421
+ const startServer = deps.startAppiumServer ?? startAppiumServer;
651422
+ const startDriver = deps.driverStart ?? driverStart;
651423
+ const kill4 = deps.killTree ?? killTree;
651424
+ const env = await getAndroidEnv();
651425
+ if (!env) {
651426
+ return {
651427
+ outcome: "skipped",
651428
+ note: "Android toolchain not ready; the first mobile-web session downloads chromedriver as needed"
651429
+ };
651430
+ }
651431
+ const pre = await preflight({ config, platform: "android" });
651432
+ if (!pre.ok)
651433
+ return { outcome: "skipped", note: pre.reason };
651434
+ const releaseLease = deps.acquireEmulatorLease ? await deps.acquireEmulatorLease() : void 0;
651435
+ let acquired;
651436
+ try {
651437
+ acquired = await acquire({
651438
+ desc,
651439
+ registry: deviceRegistry,
651440
+ sdkRoot: env.sdkRoot,
651441
+ deps: env.deviceDeps
651442
+ });
651443
+ } finally {
651444
+ releaseLease?.();
651445
+ }
651446
+ if ("skip" in acquired)
651447
+ return { outcome: "skipped", note: acquired.skip };
651448
+ let server;
651449
+ let driver;
651450
+ try {
651451
+ server = await startServer(pre.appiumEntry, config, void 0, {
651452
+ APPIUM_HOME: pre.appiumHome,
651453
+ ANDROID_HOME: env.sdkRoot,
651454
+ ANDROID_SDK_ROOT: env.sdkRoot
651455
+ }, CHROMEDRIVER_AUTODOWNLOAD_ARGS);
651456
+ driver = await startDriver(buildMobileBrowserCapabilities({
651457
+ platform: "android",
651458
+ udid: acquired.entry.udid,
651459
+ cacheDir: getCacheDir({ cacheDir: config?.cacheDir })
651460
+ }), server.port, 2, { cacheDir: config?.cacheDir });
651461
+ return {
651462
+ outcome: "warmed",
651463
+ note: `chromedriver ready for device '${acquired.entry.name}'`
651464
+ };
651465
+ } finally {
651466
+ if (driver) {
651467
+ try {
651468
+ await driver.deleteSession();
651469
+ } catch {
651470
+ }
651471
+ }
651472
+ if (server) {
651473
+ await kill4(server.process?.pid);
651474
+ }
651475
+ }
651476
+ }
650806
651477
  function specIsRouted(spec) {
650807
651478
  if (!spec || !Array.isArray(spec.tests))
650808
651479
  return false;
@@ -651150,7 +651821,8 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
651150
651821
  contextReport.resultDescription = errorMessage;
651151
651822
  return contextReport;
651152
651823
  }
651153
- clog("debug", `CONTEXT:
651824
+ if (logLevelEnabled(config, "debug"))
651825
+ clog("debug", `CONTEXT:
651154
651826
  ${JSON.stringify(context, null, 2)}`);
651155
651827
  let driver;
651156
651828
  let appiumPort;
@@ -651169,7 +651841,7 @@ ${JSON.stringify(context, null, 2)}`);
651169
651841
  env.ANDROID_HOME = appSession.androidSdkRoot;
651170
651842
  env.ANDROID_SDK_ROOT = appSession.androidSdkRoot;
651171
651843
  }
651172
- const extraArgs = mobileTarget === "android" ? ["--allow-insecure", "uiautomator2:chromedriver_autodownload"] : [];
651844
+ const extraArgs = mobileTarget === "android" ? CHROMEDRIVER_AUTODOWNLOAD_ARGS : [];
651173
651845
  let acquired;
651174
651846
  try {
651175
651847
  const server = await startAppiumServer(appSession.appiumEntry, config, void 0, env, extraArgs);
@@ -651462,7 +652134,8 @@ ${JSON.stringify(context, null, 2)}`);
651462
652134
  stepExecutionFailed = true;
651463
652135
  break;
651464
652136
  }
651465
- clog("debug", `STEP:
652137
+ if (logLevelEnabled(config, "debug"))
652138
+ clog("debug", `STEP:
651466
652139
  ${JSON.stringify(step, null, 2)}`);
651467
652140
  if (step.unsafe && runnerDetails.allowUnsafeSteps === false) {
651468
652141
  clog("warning", `Skipping unsafe step: ${step.description} in test ${test.testId} context ${context.contextId}`);
@@ -651528,7 +652201,8 @@ ${JSON.stringify(step, null, 2)}`);
651528
652201
  processRegistry,
651529
652202
  appSession
651530
652203
  });
651531
- clog("debug", `RESULT: ${r.status}
652204
+ if (logLevelEnabled(config, "debug"))
652205
+ clog("debug", `RESULT: ${r.status}
651532
652206
  ${JSON.stringify(r, null, 2)}`);
651533
652207
  r.result = r.status;
651534
652208
  r.resultDescription = r.description;
@@ -651925,7 +652599,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
651925
652599
  }
651926
652600
  return actionResult;
651927
652601
  }
651928
- async function startAppiumServer(appiumEntry, config, display, extraEnv, extraArgs) {
652602
+ async function spawnAppiumServer(appiumEntry, config, display, extraEnv, extraArgs) {
651929
652603
  const port = await findFreePort();
651930
652604
  log(config, "debug", `Starting Appium on port ${port}`);
651931
652605
  const env = display || extraEnv ? {
@@ -651945,31 +652619,41 @@ async function startAppiumServer(appiumEntry, config, display, extraEnv, extraAr
651945
652619
  });
651946
652620
  proc.stderr.on("data", () => {
651947
652621
  });
652622
+ return { port, process: proc, display };
652623
+ }
652624
+ async function startAppiumServer(appiumEntry, config, display, extraEnv, extraArgs) {
652625
+ const server = await spawnAppiumServer(appiumEntry, config, display, extraEnv, extraArgs);
651948
652626
  try {
651949
- await appiumIsReady(port);
652627
+ await appiumIsReady(server.port);
651950
652628
  } catch (error) {
651951
- await killTree(proc?.pid);
652629
+ await killTree(server.process?.pid);
651952
652630
  throw error;
651953
652631
  }
651954
- log(config, "debug", `Appium is ready on port ${port}.`);
651955
- return { port, process: proc, display };
652632
+ log(config, "debug", `Appium is ready on port ${server.port}.`);
652633
+ return server;
651956
652634
  }
651957
- async function appiumIsReady(port, timeoutMs = 12e4) {
651958
- let isReady = false;
652635
+ var STATUS_PROBE_TIMEOUT_MS = 1e4;
652636
+ async function appiumIsReady(port, timeoutMs = 12e4, deps = {}) {
652637
+ const probe = deps.probe ?? (async (p) => {
652638
+ try {
652639
+ const resp = await import_axios6.default.get(`http://127.0.0.1:${p}/status`, {
652640
+ timeout: STATUS_PROBE_TIMEOUT_MS
652641
+ });
652642
+ return resp.status === 200;
652643
+ } catch {
652644
+ return false;
652645
+ }
652646
+ });
652647
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
651959
652648
  const start = Date.now();
651960
- while (!isReady) {
652649
+ while (true) {
652650
+ if (await probe(port))
652651
+ return true;
651961
652652
  if (Date.now() - start > timeoutMs) {
651962
652653
  throw new Error(`Appium server on port ${port} failed to start within ${timeoutMs / 1e3} seconds`);
651963
652654
  }
651964
- await new Promise((resolve) => setTimeout(resolve, 1e3));
651965
- try {
651966
- let resp = await import_axios6.default.get(`http://127.0.0.1:${port}/status`);
651967
- if (resp.status === 200)
651968
- isReady = true;
651969
- } catch {
651970
- }
652655
+ await sleep4(250);
651971
652656
  }
651972
- return isReady;
651973
652657
  }
651974
652658
  async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
651975
652659
  const wdio = await loadHeavyDep("webdriverio", { ctx });
@@ -652159,6 +652843,8 @@ function telemetryNotice(config) {
652159
652843
  log(config, "info", "Doc Detective collects basic anonymous telemetry to understand product issues and usage. To disable telemetry, set 'telemetry.send' to 'false' in your .doc-detective.json config file.");
652160
652844
  }
652161
652845
  }
652846
+ var TELEMETRY_FLUSH_TIMEOUT_MS = 2e3;
652847
+ var pendingTelemetryFlush = null;
652162
652848
  function sendTelemetry(config, command, results) {
652163
652849
  if (config?.telemetry?.send === false)
652164
652850
  return;
@@ -652199,7 +652885,7 @@ function sendTelemetry(config, command, results) {
652199
652885
  const event = { distinctId, event: command, properties: telemetryData };
652200
652886
  const client = new import_posthog_node.PostHog("phc_rjV0MH3nsAd45zFISLgaKAdAXbgDeXt2mOBV2EBHomB", { host: "https://app.posthog.com" });
652201
652887
  client.capture(event);
652202
- client.shutdown();
652888
+ pendingTelemetryFlush = Promise.resolve(client.shutdown(TELEMETRY_FLUSH_TIMEOUT_MS)).then(() => void 0).catch(() => void 0);
652203
652889
  }
652204
652890
 
652205
652891
  // dist/core/index.js