doc-detective 4.7.0 → 4.8.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 (48) hide show
  1. package/dist/core/appium.d.ts +2 -1
  2. package/dist/core/appium.d.ts.map +1 -1
  3. package/dist/core/appium.js +37 -5
  4. package/dist/core/appium.js.map +1 -1
  5. package/dist/core/config.d.ts +8 -3
  6. package/dist/core/config.d.ts.map +1 -1
  7. package/dist/core/config.js +14 -6
  8. package/dist/core/config.js.map +1 -1
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +7 -10
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/core/tests/findElement.js +1 -1
  13. package/dist/core/tests/findElement.js.map +1 -1
  14. package/dist/core/tests/saveScreenshot.js +2 -2
  15. package/dist/core/tests/saveScreenshot.js.map +1 -1
  16. package/dist/core/tests/startRecording.d.ts.map +1 -1
  17. package/dist/core/tests/startRecording.js +0 -3
  18. package/dist/core/tests/startRecording.js.map +1 -1
  19. package/dist/core/tests/stopRecording.d.ts.map +1 -1
  20. package/dist/core/tests/stopRecording.js +23 -14
  21. package/dist/core/tests/stopRecording.js.map +1 -1
  22. package/dist/core/tests/typeKeys.js +2 -2
  23. package/dist/core/tests/typeKeys.js.map +1 -1
  24. package/dist/core/tests.d.ts +60 -4
  25. package/dist/core/tests.d.ts.map +1 -1
  26. package/dist/core/tests.js +771 -379
  27. package/dist/core/tests.js.map +1 -1
  28. package/dist/core/utils.d.ts +9 -1
  29. package/dist/core/utils.d.ts.map +1 -1
  30. package/dist/core/utils.js +57 -1
  31. package/dist/core/utils.js.map +1 -1
  32. package/dist/hints/context.d.ts.map +1 -1
  33. package/dist/hints/context.js +9 -2
  34. package/dist/hints/context.js.map +1 -1
  35. package/dist/hints/hints.d.ts.map +1 -1
  36. package/dist/hints/hints.js +20 -0
  37. package/dist/hints/hints.js.map +1 -1
  38. package/dist/hints/types.d.ts +5 -0
  39. package/dist/hints/types.d.ts.map +1 -1
  40. package/dist/index.cjs +591 -317
  41. package/dist/runtime/browsers.d.ts +14 -0
  42. package/dist/runtime/browsers.d.ts.map +1 -1
  43. package/dist/runtime/browsers.js +23 -0
  44. package/dist/runtime/browsers.js.map +1 -1
  45. package/dist/utils.d.ts.map +1 -1
  46. package/dist/utils.js +25 -0
  47. package/dist/utils.js.map +1 -1
  48. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -130089,6 +130089,44 @@ var init_validate = __esm({
130089
130089
  });
130090
130090
 
130091
130091
  // dist/core/utils.js
130092
+ function createAppiumPool(ports) {
130093
+ const available = [...ports];
130094
+ const waiters = [];
130095
+ return {
130096
+ acquire() {
130097
+ const port = available.shift();
130098
+ if (port !== void 0)
130099
+ return Promise.resolve(port);
130100
+ return new Promise((resolve) => waiters.push(resolve));
130101
+ },
130102
+ release(port) {
130103
+ const next = waiters.shift();
130104
+ if (next)
130105
+ next(port);
130106
+ else
130107
+ available.push(port);
130108
+ }
130109
+ };
130110
+ }
130111
+ async function runConcurrent(items, limit, fn) {
130112
+ let next = 0;
130113
+ const workerCount = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
130114
+ const workers = Array.from({ length: workerCount }, async () => {
130115
+ while (next < items.length) {
130116
+ await fn(items[next++]);
130117
+ }
130118
+ });
130119
+ await Promise.all(workers);
130120
+ }
130121
+ function rollUpResults(children) {
130122
+ if (children.some((child) => child.result === "FAIL"))
130123
+ return "FAIL";
130124
+ if (children.some((child) => child.result === "WARNING"))
130125
+ return "WARNING";
130126
+ if (children.every((child) => child.result === "SKIPPED"))
130127
+ return "SKIPPED";
130128
+ return "PASS";
130129
+ }
130092
130130
  async function findFreePort() {
130093
130131
  return new Promise((resolve, reject) => {
130094
130132
  const server = import_node_net.default.createServer();
@@ -131001,12 +131039,27 @@ var init_loader = __esm({
131001
131039
  });
131002
131040
 
131003
131041
  // dist/core/appium.js
131042
+ function appiumHomeForDriverPath(driverEntry) {
131043
+ const marker = `${import_node_path5.default.sep}node_modules${import_node_path5.default.sep}`;
131044
+ const idx = driverEntry.lastIndexOf(marker);
131045
+ return idx === -1 ? null : driverEntry.slice(0, idx);
131046
+ }
131004
131047
  function setAppiumHome(ctx = {}) {
131005
131048
  const runtimeAppium = import_node_path5.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }), "node_modules", "appium");
131006
131049
  if ((0, import_node_fs5.existsSync)(runtimeAppium)) {
131007
131050
  process.env.APPIUM_HOME = import_node_path5.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }));
131008
131051
  return;
131009
131052
  }
131053
+ for (const driverName of ["appium-chromium-driver", "appium-geckodriver"]) {
131054
+ const driverEntry = resolveHeavyDepPath(driverName, {
131055
+ cacheDir: ctx.cacheDir
131056
+ });
131057
+ const home = driverEntry ? appiumHomeForDriverPath(driverEntry) : null;
131058
+ if (home) {
131059
+ process.env.APPIUM_HOME = home;
131060
+ return;
131061
+ }
131062
+ }
131010
131063
  const corePath = import_node_path5.default.join(__dirname3, "../../node_modules");
131011
131064
  const pathArray = corePath.split("node_modules");
131012
131065
  let appiumParentPath = pathArray[0];
@@ -131026,6 +131079,7 @@ var init_appium = __esm({
131026
131079
  import_node_fs5 = require("node:fs");
131027
131080
  import_node_url4 = require("node:url");
131028
131081
  init_cacheDir();
131082
+ init_loader();
131029
131083
  __dirname3 = import_node_path5.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
131030
131084
  }
131031
131085
  });
@@ -131604,9 +131658,10 @@ async function setConfig({ config }) {
131604
131658
  }
131605
131659
  function resolveConcurrentRunners(config) {
131606
131660
  if (config.concurrentRunners === true) {
131607
- return Math.min(import_node_os3.default.cpus().length, 4);
131661
+ return Math.max(1, Math.min(import_node_os3.default.cpus().length, 4));
131608
131662
  }
131609
- return config.concurrentRunners ?? 1;
131663
+ const runners = Math.floor(Number(config.concurrentRunners));
131664
+ return Number.isFinite(runners) && runners >= 1 ? runners : 1;
131610
131665
  }
131611
131666
  async function loadDescriptions(config) {
131612
131667
  if (config?.integrations?.openApi) {
@@ -132154,8 +132209,19 @@ var browsers_exports = {};
132154
132209
  __export(browsers_exports, {
132155
132210
  BROWSER_CHANNELS: () => BROWSER_CHANNELS,
132156
132211
  ensureBrowserInstalled: () => ensureBrowserInstalled,
132157
- getInstalledBrowsers: () => getInstalledBrowsers
132212
+ getInstalledBrowsers: () => getInstalledBrowsers,
132213
+ requiredBrowserAssets: () => requiredBrowserAssets
132158
132214
  });
132215
+ function requiredBrowserAssets(name) {
132216
+ switch ((name ?? "").toLowerCase()) {
132217
+ case "chrome":
132218
+ return ["chrome", "chromedriver"];
132219
+ case "firefox":
132220
+ return ["firefox", "geckodriver"];
132221
+ default:
132222
+ return [];
132223
+ }
132224
+ }
132159
132225
  async function loadPuppeteerBrowsers(deps, ctx) {
132160
132226
  if (deps.browsersModule)
132161
132227
  return deps.browsersModule;
@@ -135110,6 +135176,7 @@ init_utils();
135110
135176
  // dist/core/tests.js
135111
135177
  var import_tree_kill = __toESM(require("tree-kill"), 1);
135112
135178
  init_loader();
135179
+ init_browsers();
135113
135180
  var import_node_os8 = __toESM(require("node:os"), 1);
135114
135181
  init_utils();
135115
135182
  var import_axios6 = __toESM(require("axios"), 1);
@@ -135787,7 +135854,7 @@ async function typeKeys({ config, step, driver }) {
135787
135854
  return result;
135788
135855
  }
135789
135856
  }
135790
- if (config.recording) {
135857
+ if (driver?.state?.recording) {
135791
135858
  let keys = [];
135792
135859
  step.type.keys.forEach((key) => {
135793
135860
  if (key.startsWith("$") && key.endsWith("$")) {
@@ -135817,7 +135884,7 @@ async function typeKeys({ config, step, driver }) {
135817
135884
  });
135818
135885
  }
135819
135886
  try {
135820
- if (config.recording) {
135887
+ if (driver?.state?.recording) {
135821
135888
  for (let i = 0; i < step.type.keys.length; i++) {
135822
135889
  await driver.keys(step.type.keys[i]);
135823
135890
  await new Promise((resolve) => setTimeout(resolve, step.type.inputDelay));
@@ -135981,7 +136048,7 @@ async function findElement({ config, step, driver, click }) {
135981
136048
  result.description += " Typed keys.";
135982
136049
  }
135983
136050
  }
135984
- if (config.recording) {
136051
+ if (driver?.state?.recording) {
135985
136052
  await wait({ config, step: { wait: 2e3 }, driver });
135986
136053
  }
135987
136054
  return result;
@@ -136883,13 +136950,13 @@ async function saveScreenshot({ config, step, driver }) {
136883
136950
  await driver.pause(100);
136884
136951
  }
136885
136952
  try {
136886
- if (config.recording) {
136953
+ if (driver?.state?.recording) {
136887
136954
  await driver.execute(() => {
136888
136955
  document.querySelector("dd-mouse-pointer").style.display = "none";
136889
136956
  });
136890
136957
  }
136891
136958
  await driver.saveScreenshot(filePath);
136892
- if (config.recording) {
136959
+ if (driver?.state?.recording) {
136893
136960
  await driver.execute(() => {
136894
136961
  document.querySelector("dd-mouse-pointer").style.display = "block";
136895
136962
  });
@@ -137124,7 +137191,6 @@ async function startRecording({ config, context, step, driver }) {
137124
137191
  return result;
137125
137192
  }
137126
137193
  if (context?.browser?.name === "chrome" && context?.browser?.headless === false) {
137127
- config.recording = {};
137128
137194
  const documentTitle = await driver.getTitle();
137129
137195
  const originalTab = await driver.getWindowHandle();
137130
137196
  await driver.execute(() => document.title = "RECORD_ME");
@@ -137133,7 +137199,6 @@ async function startRecording({ config, context, step, driver }) {
137133
137199
  await driver.switchToWindow(recorderTab.handle);
137134
137200
  await driver.url("chrome://new-tab-page");
137135
137201
  await driver.execute(() => document.title = "RECORDER");
137136
- config.recording.tab = await driver.getWindowHandle();
137137
137202
  const recorderStarted = await driver.executeAsync((baseName2, done) => {
137138
137203
  let doneCalled = false;
137139
137204
  const safeDone = (value) => {
@@ -137206,7 +137271,6 @@ async function startRecording({ config, context, step, driver }) {
137206
137271
  captureAndDownload();
137207
137272
  }, baseName);
137208
137273
  if (!recorderStarted) {
137209
- config.recording = null;
137210
137274
  result.status = "FAIL";
137211
137275
  result.description = "Failed to start recording. getDisplayMedia may have been rejected. On macOS, ensure Chrome has screen recording permission in System Preferences > Privacy & Security > Screen Recording.";
137212
137276
  log(config, "error", result.description);
@@ -137264,14 +137328,15 @@ async function stopRecording({ config, step, driver }) {
137264
137328
  return result;
137265
137329
  }
137266
137330
  step = isValidStep.object;
137267
- if (!config.recording) {
137331
+ const recording = driver?.state?.recording;
137332
+ if (!recording) {
137268
137333
  result.status = "SKIPPED";
137269
137334
  result.description = `Recording isn't started.`;
137270
137335
  return result;
137271
137336
  }
137272
137337
  try {
137273
- if (config.recording.type === "MediaRecorder") {
137274
- await driver.switchToWindow(config.recording.tab);
137338
+ if (recording.type === "MediaRecorder") {
137339
+ await driver.switchToWindow(recording.tab);
137275
137340
  const recorderExists = await driver.execute(() => {
137276
137341
  return typeof window.recorder !== "undefined" && window.recorder !== null;
137277
137342
  });
@@ -137280,35 +137345,36 @@ async function stopRecording({ config, step, driver }) {
137280
137345
  result.description = "Recording was not properly started. The recorder object doesn't exist in the browser context.";
137281
137346
  const allHandles2 = await driver.getWindowHandles();
137282
137347
  await driver.closeWindow();
137283
- const remainingHandles2 = allHandles2.filter((h) => h !== config.recording.tab);
137348
+ const remainingHandles2 = allHandles2.filter((h) => h !== recording.tab);
137284
137349
  if (remainingHandles2.length > 0) {
137285
137350
  await driver.switchToWindow(remainingHandles2[0]);
137286
137351
  }
137287
- config.recording = null;
137352
+ driver.state.recording = null;
137288
137353
  return result;
137289
137354
  }
137290
137355
  await driver.execute(() => {
137291
137356
  window.recorder.stop();
137292
137357
  });
137293
137358
  let waitCount = 0;
137294
- while (!import_node_fs13.default.existsSync(config.recording.downloadPath) && waitCount < 60) {
137359
+ while (!import_node_fs13.default.existsSync(recording.downloadPath) && waitCount < 60) {
137295
137360
  await new Promise((r) => setTimeout(r, 1e3));
137296
137361
  waitCount++;
137297
137362
  }
137298
- if (!import_node_fs13.default.existsSync(config.recording.downloadPath)) {
137363
+ if (!import_node_fs13.default.existsSync(recording.downloadPath)) {
137299
137364
  result.status = "FAIL";
137300
137365
  result.description = "Recording download timed out.";
137366
+ driver.state.recording = null;
137301
137367
  return result;
137302
137368
  }
137303
137369
  const allHandles = await driver.getWindowHandles();
137304
137370
  await driver.closeWindow();
137305
- const remainingHandles = allHandles.filter((h) => h !== config.recording.tab);
137371
+ const remainingHandles = allHandles.filter((h) => h !== recording.tab);
137306
137372
  if (remainingHandles.length > 0) {
137307
137373
  await driver.switchToWindow(remainingHandles[0]);
137308
137374
  }
137309
- const targetPath = `${config.recording.targetPath}`;
137310
- const downloadPath = `${config.recording.downloadPath}`;
137311
- const endMessage = `Finished processing file: ${config.recording.targetPath}`;
137375
+ const targetPath = `${recording.targetPath}`;
137376
+ const downloadPath = `${recording.downloadPath}`;
137377
+ const endMessage = `Finished processing file: ${recording.targetPath}`;
137312
137378
  const ffmpegArgs = ["-y", "-i", downloadPath, "-pix_fmt", "yuv420p"];
137313
137379
  if (import_node_path13.default.extname(targetPath) === ".gif") {
137314
137380
  ffmpegArgs.push("-vf", "scale=iw:-1:flags=lanczos");
@@ -137331,12 +137397,13 @@ async function stopRecording({ config, step, driver }) {
137331
137397
  }
137332
137398
  }).on("error", reject);
137333
137399
  });
137334
- config.recording = null;
137400
+ driver.state.recording = null;
137335
137401
  } else {
137336
137402
  }
137337
137403
  } catch (error) {
137338
137404
  result.status = "FAIL";
137339
137405
  result.description = `Couldn't stop recording. ${error}`;
137406
+ driver.state.recording = null;
137340
137407
  return result;
137341
137408
  }
137342
137409
  return result;
@@ -138873,6 +138940,14 @@ var driverActions2 = [
138873
138940
  "type"
138874
138941
  ];
138875
138942
  var KNOWN_BROWSERS = ["firefox", "chrome", "safari", "webkit"];
138943
+ function combinationKey(context) {
138944
+ const rawName = context?.browser?.name;
138945
+ const name = rawName === "webkit" ? "safari" : rawName || "<none>";
138946
+ return `${context?.platform}::${name}`;
138947
+ }
138948
+ function warmUpDecision(prev) {
138949
+ return prev === "failed" ? "skip" : "attempt";
138950
+ }
138876
138951
  function getDriverCapabilities({ runnerDetails, name, options }) {
138877
138952
  let capabilities = {};
138878
138953
  let args = [];
@@ -138936,8 +139011,10 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
138936
139011
  args.push(`--auto-select-desktop-capture-source=RECORD_ME`);
138937
139012
  if (options.headless)
138938
139013
  args.push("--headless", "--disable-gpu");
138939
- if (process.platform === "linux")
139014
+ if (process.platform === "linux") {
138940
139015
  args.push("--no-sandbox");
139016
+ args.push("--disable-dev-shm-usage");
139017
+ }
138941
139018
  capabilities = {
138942
139019
  platformName: runnerDetails.environment.platform,
138943
139020
  "appium:automationName": "Chromium",
@@ -138965,22 +139042,9 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
138965
139042
  }
138966
139043
  return capabilities;
138967
139044
  }
138968
- function isAppiumRequired(specs) {
138969
- let appiumRequired = false;
138970
- specs.forEach((spec) => {
138971
- spec.tests.forEach((test) => {
138972
- test.contexts.forEach((context) => {
138973
- if (isDriverRequired({ test: context })) {
138974
- appiumRequired = true;
138975
- }
138976
- });
138977
- });
138978
- });
138979
- return appiumRequired;
138980
- }
138981
139045
  function isDriverRequired({ test }) {
138982
139046
  let driverRequired = false;
138983
- test.steps.forEach((step) => {
139047
+ (test.steps || []).forEach((step) => {
138984
139048
  driverActions2.forEach((action) => {
138985
139049
  if (typeof step[action] !== "undefined")
138986
139050
  driverRequired = true;
@@ -139151,9 +139215,10 @@ async function runSpecs({ resolvedTests }) {
139151
139215
  allowUnsafeSteps: await allowUnsafeSteps({ config })
139152
139216
  };
139153
139217
  const platform = runnerDetails.environment.platform;
139154
- const availableApps = runnerDetails.availableApps;
139218
+ let availableApps = runnerDetails.availableApps;
139155
139219
  const metaValues = { specs: {} };
139156
- let appium;
139220
+ const installAttempts = /* @__PURE__ */ new Map();
139221
+ const warmUpResults = /* @__PURE__ */ new Map();
139157
139222
  const report = {
139158
139223
  summary: {
139159
139224
  specs: {
@@ -139183,293 +139248,128 @@ async function runSpecs({ resolvedTests }) {
139183
139248
  },
139184
139249
  specs: []
139185
139250
  };
139186
- const appiumRequired = isAppiumRequired(specs);
139187
- let appiumPort;
139188
- if (appiumRequired) {
139189
- setAppiumHome({ cacheDir: config?.cacheDir });
139190
- appiumPort = await findFreePort();
139191
- log(config, "debug", `Starting Appium on port ${appiumPort}`);
139192
- const appiumEntry = resolveHeavyDepPath("appium", { cacheDir: config?.cacheDir });
139193
- if (!appiumEntry) {
139194
- throw new Error("appium is not installed. The runtime pre-flight should have installed it; check DOC_DETECTIVE_CACHE_DIR / config.cacheDir or run `doc-detective install runtime appium`.");
139195
- }
139196
- appium = (0, import_node_child_process5.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(appiumPort)], {
139197
- windowsHide: true,
139198
- cwd: import_node_path18.default.join(__dirname5, "../..")
139199
- });
139200
- appium.on("error", (err) => {
139201
- log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
139202
- });
139203
- appium.stdout.on("data", (data) => {
139204
- });
139205
- appium.stderr.on("data", (data) => {
139206
- });
139207
- try {
139208
- await appiumIsReady(appiumPort);
139209
- } catch (error) {
139210
- try {
139211
- if (appium && appium.pid)
139212
- (0, import_tree_kill.default)(appium.pid);
139213
- } catch {
139214
- }
139215
- throw error;
139216
- }
139217
- log(config, "debug", "Appium is ready.");
139218
- }
139251
+ const limit = resolveConcurrentRunners(config);
139219
139252
  log(config, "info", "Running test specs.");
139253
+ const jobs = [];
139220
139254
  for (const spec of specs) {
139221
139255
  log(config, "debug", `SPEC: ${spec.specId}`);
139222
- let specReport = {
139256
+ metaValues.specs[spec.specId] ??= { tests: {} };
139257
+ const specReport = {
139223
139258
  specId: spec.specId,
139224
139259
  description: spec.description,
139225
139260
  contentPath: spec.contentPath,
139226
139261
  tests: []
139227
139262
  };
139228
- metaValues.specs[spec.specId] = { tests: {} };
139263
+ report.specs.push(specReport);
139229
139264
  for (const test of spec.tests) {
139230
139265
  log(config, "debug", `TEST: ${test.testId}`);
139231
- let testReport = {
139266
+ metaValues.specs[spec.specId].tests[test.testId] ??= { contexts: {} };
139267
+ const testReport = {
139232
139268
  testId: test.testId,
139233
139269
  description: test.description,
139234
139270
  contentPath: test.contentPath,
139235
139271
  detectSteps: test.detectSteps,
139236
- contexts: []
139272
+ contexts: new Array(test.contexts.length)
139237
139273
  };
139238
- metaValues.specs[spec.specId].tests[test.testId] = { contexts: {} };
139239
- for (const context of test.contexts) {
139240
- if (!context.platform)
139241
- context.platform = runnerDetails.environment.platform;
139242
- if (config.integrations?.openApi) {
139243
- context.openApi = [
139244
- ...context.openApi || [],
139245
- ...config.integrations.openApi
139246
- ];
139247
- }
139248
- if (!context.browser && isDriverRequired({ test: context })) {
139249
- context.browser = getDefaultBrowser({ runnerDetails });
139274
+ specReport.tests.push(testReport);
139275
+ test.contexts.forEach((context, slot) => {
139276
+ context.contextId = context.contextId || (0, import_node_crypto5.randomUUID)();
139277
+ jobs.push({ spec, test, context, contexts: testReport.contexts, slot });
139278
+ });
139279
+ }
139280
+ }
139281
+ if (limit > 1 && jobs.some((job) => job.context.steps?.some((step) => step.record !== void 0))) {
139282
+ log(config, "warning", "Tests include record steps while concurrentRunners is greater than 1. Concurrent recordings can capture the wrong window; set concurrentRunners to 1 for recording runs.");
139283
+ }
139284
+ const driverJobCount = jobs.filter((job) => isDriverRequired({ test: job.context })).length;
139285
+ let appiumServers = [];
139286
+ let appiumPool;
139287
+ if (driverJobCount > 0) {
139288
+ setAppiumHome({ cacheDir: config?.cacheDir });
139289
+ const appiumEntry = resolveHeavyDepPath("appium", {
139290
+ cacheDir: config?.cacheDir
139291
+ });
139292
+ if (!appiumEntry) {
139293
+ throw new Error("appium is not installed. The runtime pre-flight should have installed it; check DOC_DETECTIVE_CACHE_DIR / config.cacheDir or run `doc-detective install runtime appium`.");
139294
+ }
139295
+ const serverCount = Math.min(limit, driverJobCount);
139296
+ log(config, "debug", `Starting ${serverCount} Appium server(s).`);
139297
+ try {
139298
+ for (let i = 0; i < serverCount; i++) {
139299
+ appiumServers.push(await startAppiumServer(appiumEntry, config));
139300
+ }
139301
+ } catch (error) {
139302
+ for (const server of appiumServers) {
139303
+ try {
139304
+ (0, import_tree_kill.default)(server.process.pid);
139305
+ } catch {
139250
139306
  }
139251
- let contextReport = {
139252
- contextId: context.contextId || (0, import_node_crypto5.randomUUID)(),
139253
- platform: context.platform,
139254
- browser: context.browser,
139307
+ }
139308
+ throw error;
139309
+ }
139310
+ appiumPool = createAppiumPool(appiumServers.map((s) => s.port));
139311
+ }
139312
+ try {
139313
+ if (limit > 1 && appiumPool) {
139314
+ await warmUpContexts({
139315
+ jobs,
139316
+ config,
139317
+ runnerDetails,
139318
+ appiumPool,
139319
+ installAttempts,
139320
+ warmUpResults
139321
+ });
139322
+ }
139323
+ await runConcurrent(jobs, limit, async (job) => {
139324
+ try {
139325
+ job.contexts[job.slot] = await runContext({
139326
+ config,
139327
+ spec: job.spec,
139328
+ test: job.test,
139329
+ context: job.context,
139330
+ runnerDetails,
139331
+ appiumPool,
139332
+ metaValues,
139333
+ installAttempts,
139334
+ warmUpResults,
139335
+ logPrefix: limit > 1 ? `[${job.test.testId}/${job.context.contextId}]` : ""
139336
+ });
139337
+ } catch (error) {
139338
+ const detail = error?.message ?? String(error);
139339
+ log(config, "error", `Context '${job.context.contextId}' crashed: ${detail}`);
139340
+ job.contexts[job.slot] = {
139341
+ contextId: job.context.contextId,
139342
+ platform: job.context.platform,
139343
+ browser: job.context.browser,
139344
+ result: "FAIL",
139345
+ resultDescription: `Unexpected error: ${detail}`,
139255
139346
  steps: []
139256
139347
  };
139257
- metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId] = { steps: {} };
139258
- if (isDriverRequired({ test: context }) && !context.browser?.name) {
139259
- const errorMessage = `Skipping context on '${context.platform}': no supported browser is available in the current environment.`;
139260
- log(config, "warning", errorMessage);
139261
- contextReport = {
139262
- ...contextReport,
139263
- result: "SKIPPED",
139264
- resultDescription: errorMessage
139265
- };
139266
- report.summary.contexts.skipped++;
139267
- testReport.contexts.push(contextReport);
139268
- continue;
139269
- }
139270
- const supportedContext = isSupportedContext({
139271
- context,
139272
- apps: availableApps,
139273
- platform
139274
- });
139275
- if (!supportedContext) {
139276
- log(config, "info", `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`);
139277
- contextReport = { result: "SKIPPED", ...contextReport };
139278
- report.summary.contexts.skipped++;
139279
- testReport.contexts.push(contextReport);
139280
- continue;
139281
- }
139282
- log(config, "debug", `CONTEXT:
139283
- ${JSON.stringify(context, null, 2)}`);
139284
- let driver;
139285
- if (!context.steps) {
139286
- context.steps = [];
139287
- }
139288
- const driverRequired = isDriverRequired({ test: context });
139289
- if (driverRequired) {
139290
- let caps = getDriverCapabilities({
139291
- runnerDetails,
139292
- name: context.browser.name,
139293
- options: {
139294
- width: context.browser?.window?.width || 1200,
139295
- height: context.browser?.window?.height || 800,
139296
- headless: context.browser?.headless !== false
139297
- }
139298
- });
139299
- log(config, "debug", "CAPABILITIES:");
139300
- log(config, "debug", caps);
139301
- if (appiumPort === void 0) {
139302
- throw new Error("Driver requested but Appium was not started. isAppiumRequired(specs) and isDriverRequired(context) disagreed; this is a bug.");
139303
- }
139304
- try {
139305
- driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
139306
- } catch (error) {
139307
- try {
139308
- log(config, "warning", `Failed to start context '${context.browser?.name}' on '${platform}'. Retrying as headless.`);
139309
- context.browser.headless = true;
139310
- caps = getDriverCapabilities({
139311
- runnerDetails,
139312
- name: context.browser.name,
139313
- options: {
139314
- width: context.browser?.window?.width || 1200,
139315
- height: context.browser?.window?.height || 800,
139316
- headless: context.browser?.headless !== false
139317
- }
139318
- });
139319
- driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
139320
- } catch (error2) {
139321
- let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
139322
- if (context.browser?.name === "safari")
139323
- errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
139324
- log(config, "error", errorMessage);
139325
- contextReport = {
139326
- result: "SKIPPED",
139327
- resultDescription: errorMessage,
139328
- ...contextReport
139329
- };
139330
- report.summary.contexts.skipped++;
139331
- testReport.contexts.push(contextReport);
139332
- continue;
139333
- }
139334
- }
139335
- if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
139336
- await setViewportSize(context, driver);
139337
- } else if (context.browser?.window?.width || context.browser?.window?.height) {
139338
- const windowSize = await driver.getWindowSize();
139339
- await driver.setWindowSize(context.browser?.window?.width || windowSize.width, context.browser?.window?.height || windowSize.height);
139340
- }
139341
- }
139342
- let stepExecutionFailed = false;
139343
- for (let step of context.steps) {
139344
- if (!step.stepId)
139345
- step.stepId = (0, import_node_crypto5.randomUUID)();
139346
- log(config, "debug", `STEP:
139347
- ${JSON.stringify(step, null, 2)}`);
139348
- if (step.unsafe && runnerDetails.allowUnsafeSteps === false) {
139349
- log(config, "warning", `Skipping unsafe step: ${step.description} in test ${test.testId} context ${context.contextId}`);
139350
- const stepReport2 = {
139351
- ...step,
139352
- result: "SKIPPED",
139353
- resultDescription: "Skipped because unsafe steps aren't allowed."
139354
- };
139355
- contextReport.steps.push(stepReport2);
139356
- report.summary.steps.skipped++;
139357
- continue;
139358
- }
139359
- if (stepExecutionFailed) {
139360
- const stepReport2 = {
139361
- ...step,
139362
- result: "SKIPPED",
139363
- resultDescription: "Skipped due to previous failure in context."
139364
- };
139365
- contextReport.steps.push(stepReport2);
139366
- report.summary.steps.skipped++;
139348
+ }
139349
+ });
139350
+ for (const specReport of report.specs) {
139351
+ for (const testReport of specReport.tests) {
139352
+ for (const contextReport of testReport.contexts) {
139353
+ if (!contextReport)
139367
139354
  continue;
139355
+ for (const stepReport of contextReport.steps) {
139356
+ report.summary.steps[stepReport.result.toLowerCase()]++;
139368
139357
  }
139369
- metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId].steps[step.stepId] = {};
139370
- const stepResult = await runStep({
139371
- config,
139372
- context,
139373
- step,
139374
- driver,
139375
- metaValues,
139376
- options: {
139377
- openApiDefinitions: context.openApi || []
139378
- }
139379
- });
139380
- log(config, "debug", `RESULT: ${stepResult.status}
139381
- ${JSON.stringify(stepResult, null, 2)}`);
139382
- stepResult.result = stepResult.status;
139383
- stepResult.resultDescription = stepResult.description;
139384
- delete stepResult.status;
139385
- delete stepResult.description;
139386
- const stepReport = {
139387
- ...step,
139388
- ...stepResult
139389
- };
139390
- contextReport.steps.push(stepReport);
139391
- report.summary.steps[stepReport.result.toLowerCase()]++;
139392
- if (stepReport.result === "FAIL") {
139393
- stepExecutionFailed = true;
139394
- }
139395
- }
139396
- if (config.recording) {
139397
- const stopRecordStep = {
139398
- stopRecord: true,
139399
- description: "Stopping recording",
139400
- stepId: (0, import_node_crypto5.randomUUID)()
139401
- };
139402
- const stepResult = await runStep({
139403
- config,
139404
- context,
139405
- step: stopRecordStep,
139406
- driver,
139407
- options: {
139408
- openApiDefinitions: context.openApi || []
139409
- }
139410
- });
139411
- stepResult.result = stepResult.status;
139412
- stepResult.resultDescription = stepResult.description;
139413
- delete stepResult.status;
139414
- delete stepResult.description;
139415
- const stepReport = {
139416
- ...stopRecordStep,
139417
- ...stepResult
139418
- };
139419
- contextReport.steps.push(stepReport);
139420
- report.summary.steps[stepReport.result.toLowerCase()]++;
139421
- }
139422
- let contextResult;
139423
- if (contextReport.steps.find((step) => step.result === "FAIL"))
139424
- contextResult = "FAIL";
139425
- else if (contextReport.steps.find((step) => step.result === "WARNING"))
139426
- contextResult = "WARNING";
139427
- else if (contextReport.steps.length === contextReport.steps.filter((step) => step.result === "SKIPPED").length)
139428
- contextResult = "SKIPPED";
139429
- else
139430
- contextResult = "PASS";
139431
- contextReport = { result: contextResult, ...contextReport };
139432
- testReport.contexts.push(contextReport);
139433
- report.summary.contexts[contextResult.toLowerCase()]++;
139434
- if (driverRequired) {
139435
- try {
139436
- await driver.deleteSession();
139437
- } catch (error) {
139438
- log(config, "error", `Failed to delete driver session: ${error.message}`);
139439
- }
139358
+ report.summary.contexts[contextReport.result.toLowerCase()]++;
139440
139359
  }
139360
+ testReport.result = rollUpResults(testReport.contexts.filter(Boolean));
139361
+ report.summary.tests[testReport.result.toLowerCase()]++;
139441
139362
  }
139442
- let testResult;
139443
- if (testReport.contexts.find((context) => context.result === "FAIL"))
139444
- testResult = "FAIL";
139445
- else if (testReport.contexts.find((context) => context.result === "WARNING"))
139446
- testResult = "WARNING";
139447
- else if (testReport.contexts.length === testReport.contexts.filter((context) => context.result === "SKIPPED").length)
139448
- testResult = "SKIPPED";
139449
- else
139450
- testResult = "PASS";
139451
- testReport = { result: testResult, ...testReport };
139452
- specReport.tests.push(testReport);
139453
- report.summary.tests[testResult.toLowerCase()]++;
139363
+ specReport.result = rollUpResults(specReport.tests);
139364
+ report.summary.specs[specReport.result.toLowerCase()]++;
139454
139365
  }
139455
- let specResult;
139456
- if (specReport.tests.find((test) => test.result === "FAIL"))
139457
- specResult = "FAIL";
139458
- else if (specReport.tests.find((test) => test.result === "WARNING"))
139459
- specResult = "WARNING";
139460
- else if (specReport.tests.length === specReport.tests.filter((test) => test.result === "SKIPPED").length)
139461
- specResult = "SKIPPED";
139462
- else
139463
- specResult = "PASS";
139464
- specReport = { result: specResult, ...specReport };
139465
- report.specs.push(specReport);
139466
- report.summary.specs[specResult.toLowerCase()]++;
139467
- }
139468
- if (appium) {
139469
- log(config, "debug", "Closing Appium server");
139470
- try {
139471
- (0, import_tree_kill.default)(appium.pid);
139472
- } catch {
139366
+ } finally {
139367
+ for (const server of appiumServers) {
139368
+ log(config, "debug", `Closing Appium server on port ${server.port}`);
139369
+ try {
139370
+ (0, import_tree_kill.default)(server.process.pid);
139371
+ } catch {
139372
+ }
139473
139373
  }
139474
139374
  }
139475
139375
  const herettoConfigs = config?.integrations?.heretto || [];
@@ -139495,6 +139395,330 @@ ${JSON.stringify(stepResult, null, 2)}`);
139495
139395
  }
139496
139396
  return report;
139497
139397
  }
139398
+ function selectWarmUpTargets(jobs, runnerDetails) {
139399
+ const platform = runnerDetails.environment.platform;
139400
+ const seen = /* @__PURE__ */ new Set();
139401
+ const targets = [];
139402
+ for (const job of jobs) {
139403
+ const context = job.context;
139404
+ if (!context.steps)
139405
+ context.steps = [];
139406
+ if (!context.platform)
139407
+ context.platform = platform;
139408
+ if (!context.browser && isDriverRequired({ test: context })) {
139409
+ context.browser = getDefaultBrowser({ runnerDetails });
139410
+ }
139411
+ if (!isDriverRequired({ test: context }))
139412
+ continue;
139413
+ if (!context.browser?.name)
139414
+ continue;
139415
+ const combo = combinationKey(context);
139416
+ if (seen.has(combo))
139417
+ continue;
139418
+ seen.add(combo);
139419
+ targets.push({ context, combo });
139420
+ }
139421
+ return targets;
139422
+ }
139423
+ async function warmUpContexts({ jobs, config, runnerDetails, appiumPool, installAttempts, warmUpResults }) {
139424
+ const platform = runnerDetails.environment.platform;
139425
+ for (const { context } of selectWarmUpTargets(jobs, runnerDetails)) {
139426
+ const combo = combinationKey(context);
139427
+ let supported = isSupportedContext({
139428
+ context,
139429
+ apps: runnerDetails.availableApps,
139430
+ platform
139431
+ });
139432
+ if (!supported && context.platform === platform && Array.isArray(context?.steps) && requiredBrowserAssets(context.browser?.name).length > 0) {
139433
+ const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
139434
+ const outcome = await ensureContextBrowserInstalled({
139435
+ browserName: context.browser?.name,
139436
+ config,
139437
+ installAttempts,
139438
+ deps: {
139439
+ ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
139440
+ log
139441
+ }
139442
+ });
139443
+ if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
139444
+ clearAppCache(config);
139445
+ runnerDetails.availableApps = await getAvailableApps({ config });
139446
+ supported = isSupportedContext({
139447
+ context,
139448
+ apps: runnerDetails.availableApps,
139449
+ platform
139450
+ });
139451
+ }
139452
+ }
139453
+ if (!supported)
139454
+ continue;
139455
+ const port = await appiumPool.acquire();
139456
+ let warmDriver;
139457
+ try {
139458
+ const options = {
139459
+ width: context.browser?.window?.width || 1200,
139460
+ height: context.browser?.window?.height || 800,
139461
+ headless: context.browser?.headless !== false
139462
+ };
139463
+ try {
139464
+ warmDriver = await driverStart(getDriverCapabilities({
139465
+ runnerDetails,
139466
+ name: context.browser.name,
139467
+ options
139468
+ }), port, 4, { cacheDir: config?.cacheDir });
139469
+ } catch {
139470
+ log(config, "warning", `Warm-up for ${combo} failed headed; retrying headless.`);
139471
+ warmDriver = await driverStart(getDriverCapabilities({
139472
+ runnerDetails,
139473
+ name: context.browser.name,
139474
+ options: { ...options, headless: true }
139475
+ }), port, 4, { cacheDir: config?.cacheDir });
139476
+ }
139477
+ warmUpResults.set(combo, "ok");
139478
+ log(config, "debug", `Warm-up succeeded for ${combo}.`);
139479
+ } catch (error) {
139480
+ warmUpResults.set(combo, "failed");
139481
+ log(config, "warning", `Warm-up failed for ${combo}; contexts using it will be skipped: ${error?.message ?? String(error)}`);
139482
+ } finally {
139483
+ if (warmDriver) {
139484
+ try {
139485
+ await warmDriver.deleteSession();
139486
+ } catch {
139487
+ }
139488
+ }
139489
+ appiumPool.release(port);
139490
+ }
139491
+ }
139492
+ }
139493
+ async function runContext({ config, spec, test, context, runnerDetails, appiumPool, metaValues, installAttempts, warmUpResults, logPrefix = "" }) {
139494
+ const platform = runnerDetails.environment.platform;
139495
+ let availableApps = runnerDetails.availableApps;
139496
+ const clog = (level, message) => log(config, level, logPrefix && typeof message === "string" ? `${logPrefix} ${message}` : message);
139497
+ if (!context.steps) {
139498
+ context.steps = [];
139499
+ }
139500
+ if (!context.platform)
139501
+ context.platform = runnerDetails.environment.platform;
139502
+ if (config.integrations?.openApi) {
139503
+ context.openApi = [
139504
+ ...context.openApi || [],
139505
+ ...config.integrations.openApi
139506
+ ];
139507
+ }
139508
+ if (!context.browser && isDriverRequired({ test: context })) {
139509
+ context.browser = getDefaultBrowser({ runnerDetails });
139510
+ }
139511
+ const contextReport = {
139512
+ contextId: context.contextId,
139513
+ platform: context.platform,
139514
+ browser: context.browser,
139515
+ steps: []
139516
+ };
139517
+ metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId] ??= { steps: {} };
139518
+ if (isDriverRequired({ test: context }) && !context.browser?.name) {
139519
+ const errorMessage = `Skipping context on '${context.platform}': no supported browser is available in the current environment.`;
139520
+ clog("warning", errorMessage);
139521
+ contextReport.result = "SKIPPED";
139522
+ contextReport.resultDescription = errorMessage;
139523
+ return contextReport;
139524
+ }
139525
+ let supportedContext = isSupportedContext({
139526
+ context,
139527
+ apps: availableApps,
139528
+ platform
139529
+ });
139530
+ let freshInstallRedetected = false;
139531
+ if (!supportedContext && context.platform === platform && // Mirror isSupportedContext's own guard: isDriverRequired iterates
139532
+ // context.steps, so a malformed context without a steps array would
139533
+ // otherwise crash here instead of skipping cleanly.
139534
+ Array.isArray(context?.steps) && isDriverRequired({ test: context }) && requiredBrowserAssets(context.browser?.name).length > 0) {
139535
+ const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
139536
+ const outcome = await ensureContextBrowserInstalled({
139537
+ browserName: context.browser?.name,
139538
+ config,
139539
+ installAttempts,
139540
+ deps: {
139541
+ ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
139542
+ log
139543
+ }
139544
+ });
139545
+ if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
139546
+ freshInstallRedetected = true;
139547
+ clearAppCache(config);
139548
+ availableApps = await getAvailableApps({ config });
139549
+ runnerDetails.availableApps = availableApps;
139550
+ supportedContext = isSupportedContext({
139551
+ context,
139552
+ apps: availableApps,
139553
+ platform
139554
+ });
139555
+ }
139556
+ }
139557
+ if (!supportedContext) {
139558
+ 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)}}`;
139559
+ clog(freshInstallRedetected ? "warning" : "info", errorMessage);
139560
+ contextReport.result = "SKIPPED";
139561
+ contextReport.resultDescription = errorMessage;
139562
+ return contextReport;
139563
+ }
139564
+ clog("debug", `CONTEXT:
139565
+ ${JSON.stringify(context, null, 2)}`);
139566
+ let driver;
139567
+ let appiumPort;
139568
+ const driverRequired = isDriverRequired({ test: context });
139569
+ if (driverRequired && !appiumPool) {
139570
+ throw new Error("Driver requested but no Appium server pool was created; driverJobCount and isDriverRequired(context) disagreed; this is a bug.");
139571
+ }
139572
+ const combo = combinationKey(context);
139573
+ try {
139574
+ if (driverRequired) {
139575
+ if (warmUpDecision(warmUpResults.get(combo)) === "skip") {
139576
+ const errorMessage = `Skipping context '${context.browser?.name}' on '${context.platform}': this context combination could not start a driver earlier in this run.`;
139577
+ clog("warning", errorMessage);
139578
+ contextReport.result = "SKIPPED";
139579
+ contextReport.resultDescription = errorMessage;
139580
+ return contextReport;
139581
+ }
139582
+ appiumPort = await appiumPool.acquire();
139583
+ let caps = getDriverCapabilities({
139584
+ runnerDetails,
139585
+ name: context.browser.name,
139586
+ options: {
139587
+ width: context.browser?.window?.width || 1200,
139588
+ height: context.browser?.window?.height || 800,
139589
+ headless: context.browser?.headless !== false
139590
+ }
139591
+ });
139592
+ clog("debug", "CAPABILITIES:");
139593
+ clog("debug", caps);
139594
+ try {
139595
+ driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
139596
+ } catch (error) {
139597
+ try {
139598
+ clog("warning", `Failed to start context '${context.browser?.name}' on '${platform}'. Retrying as headless.`);
139599
+ context.browser.headless = true;
139600
+ caps = getDriverCapabilities({
139601
+ runnerDetails,
139602
+ name: context.browser.name,
139603
+ options: {
139604
+ width: context.browser?.window?.width || 1200,
139605
+ height: context.browser?.window?.height || 800,
139606
+ headless: context.browser?.headless !== false
139607
+ }
139608
+ });
139609
+ driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
139610
+ } catch (error2) {
139611
+ let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
139612
+ if (context.browser?.name === "safari" || context.browser?.name === "webkit")
139613
+ errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
139614
+ clog("error", errorMessage);
139615
+ if (!warmUpResults.has(combo))
139616
+ warmUpResults.set(combo, "failed");
139617
+ contextReport.result = "SKIPPED";
139618
+ contextReport.resultDescription = errorMessage;
139619
+ return contextReport;
139620
+ }
139621
+ }
139622
+ if (!warmUpResults.has(combo))
139623
+ warmUpResults.set(combo, "ok");
139624
+ if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
139625
+ await setViewportSize(context, driver);
139626
+ } else if (context.browser?.window?.width || context.browser?.window?.height) {
139627
+ const windowSize = await driver.getWindowSize();
139628
+ await driver.setWindowSize(context.browser?.window?.width || windowSize.width, context.browser?.window?.height || windowSize.height);
139629
+ }
139630
+ }
139631
+ let stepExecutionFailed = false;
139632
+ for (let step of context.steps) {
139633
+ if (!step.stepId)
139634
+ step.stepId = (0, import_node_crypto5.randomUUID)();
139635
+ clog("debug", `STEP:
139636
+ ${JSON.stringify(step, null, 2)}`);
139637
+ if (step.unsafe && runnerDetails.allowUnsafeSteps === false) {
139638
+ clog("warning", `Skipping unsafe step: ${step.description} in test ${test.testId} context ${context.contextId}`);
139639
+ const stepReport2 = {
139640
+ ...step,
139641
+ result: "SKIPPED",
139642
+ resultDescription: "Skipped because unsafe steps aren't allowed."
139643
+ };
139644
+ contextReport.steps.push(stepReport2);
139645
+ continue;
139646
+ }
139647
+ if (stepExecutionFailed) {
139648
+ const stepReport2 = {
139649
+ ...step,
139650
+ result: "SKIPPED",
139651
+ resultDescription: "Skipped due to previous failure in context."
139652
+ };
139653
+ contextReport.steps.push(stepReport2);
139654
+ continue;
139655
+ }
139656
+ metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId].steps[step.stepId] = {};
139657
+ const stepResult = await runStep({
139658
+ config,
139659
+ context,
139660
+ step,
139661
+ driver,
139662
+ metaValues,
139663
+ options: {
139664
+ openApiDefinitions: context.openApi || []
139665
+ }
139666
+ });
139667
+ clog("debug", `RESULT: ${stepResult.status}
139668
+ ${JSON.stringify(stepResult, null, 2)}`);
139669
+ stepResult.result = stepResult.status;
139670
+ stepResult.resultDescription = stepResult.description;
139671
+ delete stepResult.status;
139672
+ delete stepResult.description;
139673
+ const stepReport = {
139674
+ ...step,
139675
+ ...stepResult
139676
+ };
139677
+ contextReport.steps.push(stepReport);
139678
+ if (stepReport.result === "FAIL") {
139679
+ stepExecutionFailed = true;
139680
+ }
139681
+ }
139682
+ if (driver?.state?.recording) {
139683
+ const stopRecordStep = {
139684
+ stopRecord: true,
139685
+ description: "Stopping recording",
139686
+ stepId: (0, import_node_crypto5.randomUUID)()
139687
+ };
139688
+ const stepResult = await runStep({
139689
+ config,
139690
+ context,
139691
+ step: stopRecordStep,
139692
+ driver,
139693
+ options: {
139694
+ openApiDefinitions: context.openApi || []
139695
+ }
139696
+ });
139697
+ stepResult.result = stepResult.status;
139698
+ stepResult.resultDescription = stepResult.description;
139699
+ delete stepResult.status;
139700
+ delete stepResult.description;
139701
+ const stepReport = {
139702
+ ...stopRecordStep,
139703
+ ...stepResult
139704
+ };
139705
+ contextReport.steps.push(stepReport);
139706
+ }
139707
+ } finally {
139708
+ if (driver) {
139709
+ try {
139710
+ await driver.deleteSession();
139711
+ } catch (error) {
139712
+ clog("error", `Failed to delete driver session: ${error.message}`);
139713
+ }
139714
+ }
139715
+ if (appiumPort !== void 0 && appiumPool) {
139716
+ appiumPool.release(appiumPort);
139717
+ }
139718
+ }
139719
+ contextReport.result = rollUpResults(contextReport.steps);
139720
+ return contextReport;
139721
+ }
139498
139722
  async function runStep({ config = {}, context = {}, step, driver, metaValues = {}, options = {} }) {
139499
139723
  let actionResult;
139500
139724
  step = replaceEnvs(step);
@@ -139545,7 +139769,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
139545
139769
  step,
139546
139770
  driver
139547
139771
  });
139548
- config.recording = actionResult.recording;
139772
+ driver.state.recording = actionResult.recording ?? null;
139549
139773
  } else if (typeof step.runCode !== "undefined") {
139550
139774
  actionResult = await runCode({ config, step });
139551
139775
  } else if (typeof step.runShell !== "undefined") {
@@ -139570,7 +139794,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
139570
139794
  description: `Unknown step action: ${JSON.stringify(step)}`
139571
139795
  };
139572
139796
  }
139573
- if (config?.recording) {
139797
+ if (driver?.state?.recording) {
139574
139798
  const currentUrl = await driver.getUrl();
139575
139799
  if (currentUrl !== driver.state.url) {
139576
139800
  driver.state.url = currentUrl;
@@ -139592,6 +139816,33 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
139592
139816
  }
139593
139817
  return actionResult;
139594
139818
  }
139819
+ async function startAppiumServer(appiumEntry, config) {
139820
+ const port = await findFreePort();
139821
+ log(config, "debug", `Starting Appium on port ${port}`);
139822
+ const proc = (0, import_node_child_process5.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port)], {
139823
+ windowsHide: true,
139824
+ cwd: import_node_path18.default.join(__dirname5, "../..")
139825
+ });
139826
+ proc.on("error", (err) => {
139827
+ log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
139828
+ });
139829
+ proc.stdout.on("data", () => {
139830
+ });
139831
+ proc.stderr.on("data", () => {
139832
+ });
139833
+ try {
139834
+ await appiumIsReady(port);
139835
+ } catch (error) {
139836
+ try {
139837
+ if (proc && proc.pid)
139838
+ (0, import_tree_kill.default)(proc.pid);
139839
+ } catch {
139840
+ }
139841
+ throw error;
139842
+ }
139843
+ log(config, "debug", `Appium is ready on port ${port}.`);
139844
+ return { port, process: proc };
139845
+ }
139595
139846
  async function appiumIsReady(port, timeoutMs = 12e4) {
139596
139847
  let isReady = false;
139597
139848
  const start = Date.now();
@@ -139610,6 +139861,7 @@ async function appiumIsReady(port, timeoutMs = 12e4) {
139610
139861
  return isReady;
139611
139862
  }
139612
139863
  async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
139864
+ const TRANSIENT = /ECONNREFUSED|ECONNRESET|socket hang up|could not proxy command|crashed during startup|cannot connect to|DevToolsActivePort|session not created/i;
139613
139865
  const wdio = await loadHeavyDep("webdriverio", { ctx });
139614
139866
  let lastError;
139615
139867
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -139626,11 +139878,11 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
139626
139878
  waitforTimeout: 12e4
139627
139879
  // 2 minutes
139628
139880
  });
139629
- driver.state = { url: "", x: null, y: null };
139881
+ driver.state = { url: "", x: null, y: null, recording: null };
139630
139882
  return driver;
139631
139883
  } catch (err) {
139632
139884
  lastError = err;
139633
- if (!/ECONNREFUSED/.test(String(err && err.message)))
139885
+ if (!TRANSIENT.test(String(err && err.message)))
139634
139886
  throw err;
139635
139887
  if (attempt < maxAttempts) {
139636
139888
  await new Promise((r) => setTimeout(r, 500 * attempt));
@@ -139667,6 +139919,31 @@ async function ensureChromeAvailable(config, deps) {
139667
139919
  }
139668
139920
  return availableApps;
139669
139921
  }
139922
+ async function ensureContextBrowserInstalled({ browserName, config, installAttempts, deps }) {
139923
+ const key = (browserName ?? "<none>").toLowerCase();
139924
+ const cached = installAttempts.get(key);
139925
+ if (cached)
139926
+ return cached;
139927
+ const assets = requiredBrowserAssets(browserName);
139928
+ if (assets.length === 0) {
139929
+ installAttempts.set(key, "notInstallable");
139930
+ return "notInstallable";
139931
+ }
139932
+ const ctx = { cacheDir: config?.cacheDir };
139933
+ const logger = (msg, level = "info") => deps.log?.(config, level === "warn" ? "warning" : level, msg);
139934
+ try {
139935
+ deps.log?.(config, "info", `Browser '${browserName}' is not available; attempting on-demand install of: ${assets.join(", ")}.`);
139936
+ for (const asset of assets) {
139937
+ await deps.ensureBrowser(asset, { ctx, deps: { logger } });
139938
+ }
139939
+ installAttempts.set(key, "installed");
139940
+ return "installed";
139941
+ } catch (err) {
139942
+ deps.log?.(config, "warning", `On-demand install for '${browserName}' failed: ${err?.message ?? err}`);
139943
+ installAttempts.set(key, "failed");
139944
+ return "failed";
139945
+ }
139946
+ }
139670
139947
  async function getRunner(options = {}) {
139671
139948
  const environment = getEnvironment();
139672
139949
  const config = { ...options.config, environment };
@@ -139880,22 +140157,19 @@ async function runTests(config, options = {}) {
139880
140157
  if (needs.browsers.size > 0) {
139881
140158
  try {
139882
140159
  const { getAvailableApps: getAvailableApps2, clearAppCache: clearAppCache2 } = await Promise.resolve().then(() => (init_config(), config_exports));
139883
- const { ensureBrowserInstalled: ensureBrowserInstalled2 } = await Promise.resolve().then(() => (init_browsers(), browsers_exports));
140160
+ const { ensureBrowserInstalled: ensureBrowserInstalled2, requiredBrowserAssets: requiredBrowserAssets2 } = await Promise.resolve().then(() => (init_browsers(), browsers_exports));
139884
140161
  const available = await getAvailableApps2({ config });
139885
140162
  const availableNames = new Set(available.map((a) => a.name));
139886
140163
  let installedAnything = false;
139887
140164
  for (const browser of needs.browsers) {
139888
140165
  if (availableNames.has(browser))
139889
140166
  continue;
139890
- if (browser === "chrome") {
139891
- await ensureBrowserInstalled2("chrome", { ctx, deps: { logger: preflightLogger } });
139892
- await ensureBrowserInstalled2("chromedriver", { ctx, deps: { logger: preflightLogger } });
139893
- installedAnything = true;
139894
- } else if (browser === "firefox") {
139895
- await ensureBrowserInstalled2("firefox", { ctx, deps: { logger: preflightLogger } });
139896
- await ensureBrowserInstalled2("geckodriver", { ctx, deps: { logger: preflightLogger } });
139897
- installedAnything = true;
140167
+ const assets = requiredBrowserAssets2(browser);
140168
+ for (const asset of assets) {
140169
+ await ensureBrowserInstalled2(asset, { ctx, deps: { logger: preflightLogger } });
139898
140170
  }
140171
+ if (assets.length > 0)
140172
+ installedAnything = true;
139899
140173
  }
139900
140174
  if (installedAnything)
139901
140175
  clearAppCache2(config);