doc-detective 4.7.0-runtime-dependency-detection.1 → 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 (41) 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/tests/findElement.js +1 -1
  10. package/dist/core/tests/findElement.js.map +1 -1
  11. package/dist/core/tests/saveScreenshot.js +2 -2
  12. package/dist/core/tests/saveScreenshot.js.map +1 -1
  13. package/dist/core/tests/startRecording.d.ts.map +1 -1
  14. package/dist/core/tests/startRecording.js +0 -3
  15. package/dist/core/tests/startRecording.js.map +1 -1
  16. package/dist/core/tests/stopRecording.d.ts.map +1 -1
  17. package/dist/core/tests/stopRecording.js +23 -14
  18. package/dist/core/tests/stopRecording.js.map +1 -1
  19. package/dist/core/tests/typeKeys.js +2 -2
  20. package/dist/core/tests/typeKeys.js.map +1 -1
  21. package/dist/core/tests.d.ts +20 -4
  22. package/dist/core/tests.d.ts.map +1 -1
  23. package/dist/core/tests.js +700 -467
  24. package/dist/core/tests.js.map +1 -1
  25. package/dist/core/utils.d.ts +9 -1
  26. package/dist/core/utils.d.ts.map +1 -1
  27. package/dist/core/utils.js +57 -1
  28. package/dist/core/utils.js.map +1 -1
  29. package/dist/hints/context.d.ts.map +1 -1
  30. package/dist/hints/context.js +9 -2
  31. package/dist/hints/context.js.map +1 -1
  32. package/dist/hints/hints.d.ts.map +1 -1
  33. package/dist/hints/hints.js +20 -0
  34. package/dist/hints/hints.js.map +1 -1
  35. package/dist/hints/types.d.ts +5 -0
  36. package/dist/hints/types.d.ts.map +1 -1
  37. package/dist/index.cjs +536 -355
  38. package/dist/utils.d.ts.map +1 -1
  39. package/dist/utils.js +25 -0
  40. package/dist/utils.js.map +1 -1
  41. 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) {
@@ -135799,7 +135854,7 @@ async function typeKeys({ config, step, driver }) {
135799
135854
  return result;
135800
135855
  }
135801
135856
  }
135802
- if (config.recording) {
135857
+ if (driver?.state?.recording) {
135803
135858
  let keys = [];
135804
135859
  step.type.keys.forEach((key) => {
135805
135860
  if (key.startsWith("$") && key.endsWith("$")) {
@@ -135829,7 +135884,7 @@ async function typeKeys({ config, step, driver }) {
135829
135884
  });
135830
135885
  }
135831
135886
  try {
135832
- if (config.recording) {
135887
+ if (driver?.state?.recording) {
135833
135888
  for (let i = 0; i < step.type.keys.length; i++) {
135834
135889
  await driver.keys(step.type.keys[i]);
135835
135890
  await new Promise((resolve) => setTimeout(resolve, step.type.inputDelay));
@@ -135993,7 +136048,7 @@ async function findElement({ config, step, driver, click }) {
135993
136048
  result.description += " Typed keys.";
135994
136049
  }
135995
136050
  }
135996
- if (config.recording) {
136051
+ if (driver?.state?.recording) {
135997
136052
  await wait({ config, step: { wait: 2e3 }, driver });
135998
136053
  }
135999
136054
  return result;
@@ -136895,13 +136950,13 @@ async function saveScreenshot({ config, step, driver }) {
136895
136950
  await driver.pause(100);
136896
136951
  }
136897
136952
  try {
136898
- if (config.recording) {
136953
+ if (driver?.state?.recording) {
136899
136954
  await driver.execute(() => {
136900
136955
  document.querySelector("dd-mouse-pointer").style.display = "none";
136901
136956
  });
136902
136957
  }
136903
136958
  await driver.saveScreenshot(filePath);
136904
- if (config.recording) {
136959
+ if (driver?.state?.recording) {
136905
136960
  await driver.execute(() => {
136906
136961
  document.querySelector("dd-mouse-pointer").style.display = "block";
136907
136962
  });
@@ -137136,7 +137191,6 @@ async function startRecording({ config, context, step, driver }) {
137136
137191
  return result;
137137
137192
  }
137138
137193
  if (context?.browser?.name === "chrome" && context?.browser?.headless === false) {
137139
- config.recording = {};
137140
137194
  const documentTitle = await driver.getTitle();
137141
137195
  const originalTab = await driver.getWindowHandle();
137142
137196
  await driver.execute(() => document.title = "RECORD_ME");
@@ -137145,7 +137199,6 @@ async function startRecording({ config, context, step, driver }) {
137145
137199
  await driver.switchToWindow(recorderTab.handle);
137146
137200
  await driver.url("chrome://new-tab-page");
137147
137201
  await driver.execute(() => document.title = "RECORDER");
137148
- config.recording.tab = await driver.getWindowHandle();
137149
137202
  const recorderStarted = await driver.executeAsync((baseName2, done) => {
137150
137203
  let doneCalled = false;
137151
137204
  const safeDone = (value) => {
@@ -137218,7 +137271,6 @@ async function startRecording({ config, context, step, driver }) {
137218
137271
  captureAndDownload();
137219
137272
  }, baseName);
137220
137273
  if (!recorderStarted) {
137221
- config.recording = null;
137222
137274
  result.status = "FAIL";
137223
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.";
137224
137276
  log(config, "error", result.description);
@@ -137276,14 +137328,15 @@ async function stopRecording({ config, step, driver }) {
137276
137328
  return result;
137277
137329
  }
137278
137330
  step = isValidStep.object;
137279
- if (!config.recording) {
137331
+ const recording = driver?.state?.recording;
137332
+ if (!recording) {
137280
137333
  result.status = "SKIPPED";
137281
137334
  result.description = `Recording isn't started.`;
137282
137335
  return result;
137283
137336
  }
137284
137337
  try {
137285
- if (config.recording.type === "MediaRecorder") {
137286
- await driver.switchToWindow(config.recording.tab);
137338
+ if (recording.type === "MediaRecorder") {
137339
+ await driver.switchToWindow(recording.tab);
137287
137340
  const recorderExists = await driver.execute(() => {
137288
137341
  return typeof window.recorder !== "undefined" && window.recorder !== null;
137289
137342
  });
@@ -137292,35 +137345,36 @@ async function stopRecording({ config, step, driver }) {
137292
137345
  result.description = "Recording was not properly started. The recorder object doesn't exist in the browser context.";
137293
137346
  const allHandles2 = await driver.getWindowHandles();
137294
137347
  await driver.closeWindow();
137295
- const remainingHandles2 = allHandles2.filter((h) => h !== config.recording.tab);
137348
+ const remainingHandles2 = allHandles2.filter((h) => h !== recording.tab);
137296
137349
  if (remainingHandles2.length > 0) {
137297
137350
  await driver.switchToWindow(remainingHandles2[0]);
137298
137351
  }
137299
- config.recording = null;
137352
+ driver.state.recording = null;
137300
137353
  return result;
137301
137354
  }
137302
137355
  await driver.execute(() => {
137303
137356
  window.recorder.stop();
137304
137357
  });
137305
137358
  let waitCount = 0;
137306
- while (!import_node_fs13.default.existsSync(config.recording.downloadPath) && waitCount < 60) {
137359
+ while (!import_node_fs13.default.existsSync(recording.downloadPath) && waitCount < 60) {
137307
137360
  await new Promise((r) => setTimeout(r, 1e3));
137308
137361
  waitCount++;
137309
137362
  }
137310
- if (!import_node_fs13.default.existsSync(config.recording.downloadPath)) {
137363
+ if (!import_node_fs13.default.existsSync(recording.downloadPath)) {
137311
137364
  result.status = "FAIL";
137312
137365
  result.description = "Recording download timed out.";
137366
+ driver.state.recording = null;
137313
137367
  return result;
137314
137368
  }
137315
137369
  const allHandles = await driver.getWindowHandles();
137316
137370
  await driver.closeWindow();
137317
- const remainingHandles = allHandles.filter((h) => h !== config.recording.tab);
137371
+ const remainingHandles = allHandles.filter((h) => h !== recording.tab);
137318
137372
  if (remainingHandles.length > 0) {
137319
137373
  await driver.switchToWindow(remainingHandles[0]);
137320
137374
  }
137321
- const targetPath = `${config.recording.targetPath}`;
137322
- const downloadPath = `${config.recording.downloadPath}`;
137323
- 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}`;
137324
137378
  const ffmpegArgs = ["-y", "-i", downloadPath, "-pix_fmt", "yuv420p"];
137325
137379
  if (import_node_path13.default.extname(targetPath) === ".gif") {
137326
137380
  ffmpegArgs.push("-vf", "scale=iw:-1:flags=lanczos");
@@ -137343,12 +137397,13 @@ async function stopRecording({ config, step, driver }) {
137343
137397
  }
137344
137398
  }).on("error", reject);
137345
137399
  });
137346
- config.recording = null;
137400
+ driver.state.recording = null;
137347
137401
  } else {
137348
137402
  }
137349
137403
  } catch (error) {
137350
137404
  result.status = "FAIL";
137351
137405
  result.description = `Couldn't stop recording. ${error}`;
137406
+ driver.state.recording = null;
137352
137407
  return result;
137353
137408
  }
137354
137409
  return result;
@@ -138956,8 +139011,10 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
138956
139011
  args.push(`--auto-select-desktop-capture-source=RECORD_ME`);
138957
139012
  if (options.headless)
138958
139013
  args.push("--headless", "--disable-gpu");
138959
- if (process.platform === "linux")
139014
+ if (process.platform === "linux") {
138960
139015
  args.push("--no-sandbox");
139016
+ args.push("--disable-dev-shm-usage");
139017
+ }
138961
139018
  capabilities = {
138962
139019
  platformName: runnerDetails.environment.platform,
138963
139020
  "appium:automationName": "Chromium",
@@ -138985,22 +139042,9 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
138985
139042
  }
138986
139043
  return capabilities;
138987
139044
  }
138988
- function isAppiumRequired(specs) {
138989
- let appiumRequired = false;
138990
- specs.forEach((spec) => {
138991
- spec.tests.forEach((test) => {
138992
- test.contexts.forEach((context) => {
138993
- if (isDriverRequired({ test: context })) {
138994
- appiumRequired = true;
138995
- }
138996
- });
138997
- });
138998
- });
138999
- return appiumRequired;
139000
- }
139001
139045
  function isDriverRequired({ test }) {
139002
139046
  let driverRequired = false;
139003
- test.steps.forEach((step) => {
139047
+ (test.steps || []).forEach((step) => {
139004
139048
  driverActions2.forEach((action) => {
139005
139049
  if (typeof step[action] !== "undefined")
139006
139050
  driverRequired = true;
@@ -139175,7 +139219,6 @@ async function runSpecs({ resolvedTests }) {
139175
139219
  const metaValues = { specs: {} };
139176
139220
  const installAttempts = /* @__PURE__ */ new Map();
139177
139221
  const warmUpResults = /* @__PURE__ */ new Map();
139178
- let appium;
139179
139222
  const report = {
139180
139223
  summary: {
139181
139224
  specs: {
@@ -139205,342 +139248,128 @@ async function runSpecs({ resolvedTests }) {
139205
139248
  },
139206
139249
  specs: []
139207
139250
  };
139208
- const appiumRequired = isAppiumRequired(specs);
139209
- let appiumPort;
139210
- if (appiumRequired) {
139211
- setAppiumHome({ cacheDir: config?.cacheDir });
139212
- appiumPort = await findFreePort();
139213
- log(config, "debug", `Starting Appium on port ${appiumPort}`);
139214
- const appiumEntry = resolveHeavyDepPath("appium", { cacheDir: config?.cacheDir });
139215
- if (!appiumEntry) {
139216
- 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`.");
139217
- }
139218
- appium = (0, import_node_child_process5.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(appiumPort)], {
139219
- windowsHide: true,
139220
- cwd: import_node_path18.default.join(__dirname5, "../..")
139221
- });
139222
- appium.on("error", (err) => {
139223
- log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
139224
- });
139225
- appium.stdout.on("data", (data) => {
139226
- });
139227
- appium.stderr.on("data", (data) => {
139228
- });
139229
- try {
139230
- await appiumIsReady(appiumPort);
139231
- } catch (error) {
139232
- try {
139233
- if (appium && appium.pid)
139234
- (0, import_tree_kill.default)(appium.pid);
139235
- } catch {
139236
- }
139237
- throw error;
139238
- }
139239
- log(config, "debug", "Appium is ready.");
139240
- }
139251
+ const limit = resolveConcurrentRunners(config);
139241
139252
  log(config, "info", "Running test specs.");
139253
+ const jobs = [];
139242
139254
  for (const spec of specs) {
139243
139255
  log(config, "debug", `SPEC: ${spec.specId}`);
139244
- let specReport = {
139256
+ metaValues.specs[spec.specId] ??= { tests: {} };
139257
+ const specReport = {
139245
139258
  specId: spec.specId,
139246
139259
  description: spec.description,
139247
139260
  contentPath: spec.contentPath,
139248
139261
  tests: []
139249
139262
  };
139250
- metaValues.specs[spec.specId] = { tests: {} };
139263
+ report.specs.push(specReport);
139251
139264
  for (const test of spec.tests) {
139252
139265
  log(config, "debug", `TEST: ${test.testId}`);
139253
- let testReport = {
139266
+ metaValues.specs[spec.specId].tests[test.testId] ??= { contexts: {} };
139267
+ const testReport = {
139254
139268
  testId: test.testId,
139255
139269
  description: test.description,
139256
139270
  contentPath: test.contentPath,
139257
139271
  detectSteps: test.detectSteps,
139258
- contexts: []
139272
+ contexts: new Array(test.contexts.length)
139259
139273
  };
139260
- metaValues.specs[spec.specId].tests[test.testId] = { contexts: {} };
139261
- for (const context of test.contexts) {
139262
- if (!context.platform)
139263
- context.platform = runnerDetails.environment.platform;
139264
- if (config.integrations?.openApi) {
139265
- context.openApi = [
139266
- ...context.openApi || [],
139267
- ...config.integrations.openApi
139268
- ];
139269
- }
139270
- if (!context.browser && isDriverRequired({ test: context })) {
139271
- 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 {
139272
139306
  }
139273
- let contextReport = {
139274
- contextId: context.contextId || (0, import_node_crypto5.randomUUID)(),
139275
- platform: context.platform,
139276
- 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}`,
139277
139346
  steps: []
139278
139347
  };
139279
- metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId] = { steps: {} };
139280
- if (isDriverRequired({ test: context }) && !context.browser?.name) {
139281
- const errorMessage = `Skipping context on '${context.platform}': no supported browser is available in the current environment.`;
139282
- log(config, "warning", errorMessage);
139283
- contextReport = {
139284
- ...contextReport,
139285
- result: "SKIPPED",
139286
- resultDescription: errorMessage
139287
- };
139288
- report.summary.contexts.skipped++;
139289
- testReport.contexts.push(contextReport);
139290
- continue;
139291
- }
139292
- let supportedContext = isSupportedContext({
139293
- context,
139294
- apps: availableApps,
139295
- platform
139296
- });
139297
- let freshInstallRedetected = false;
139298
- if (!supportedContext && context.platform === platform && // Mirror isSupportedContext's own guard: isDriverRequired iterates
139299
- // context.steps, so a malformed context without a steps array would
139300
- // otherwise crash the loop here instead of skipping cleanly.
139301
- Array.isArray(context?.steps) && isDriverRequired({ test: context }) && requiredBrowserAssets(context.browser?.name).length > 0) {
139302
- const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
139303
- const outcome = await ensureContextBrowserInstalled({
139304
- browserName: context.browser?.name,
139305
- config,
139306
- installAttempts,
139307
- deps: {
139308
- ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
139309
- log
139310
- }
139311
- });
139312
- if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
139313
- freshInstallRedetected = true;
139314
- clearAppCache(config);
139315
- availableApps = await getAvailableApps({ config });
139316
- runnerDetails.availableApps = availableApps;
139317
- supportedContext = isSupportedContext({
139318
- context,
139319
- apps: availableApps,
139320
- platform
139321
- });
139322
- }
139323
- }
139324
- if (!supportedContext) {
139325
- const errorMessage = freshInstallRedetected ? `Skipping context '${context.browser?.name}' on '${context.platform}': the missing browser dependency was installed but still could not be detected.` : `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`;
139326
- log(config, freshInstallRedetected ? "warning" : "info", errorMessage);
139327
- contextReport = {
139328
- ...contextReport,
139329
- result: "SKIPPED",
139330
- resultDescription: errorMessage
139331
- };
139332
- report.summary.contexts.skipped++;
139333
- testReport.contexts.push(contextReport);
139334
- continue;
139335
- }
139336
- log(config, "debug", `CONTEXT:
139337
- ${JSON.stringify(context, null, 2)}`);
139338
- let driver;
139339
- if (!context.steps) {
139340
- context.steps = [];
139341
- }
139342
- const driverRequired = isDriverRequired({ test: context });
139343
- if (driverRequired) {
139344
- const combo = combinationKey(context);
139345
- if (warmUpDecision(warmUpResults.get(combo)) === "skip") {
139346
- const errorMessage = `Skipping context '${context.browser?.name}' on '${context.platform}': this context combination could not start a driver earlier in this run.`;
139347
- log(config, "warning", errorMessage);
139348
- contextReport = {
139349
- ...contextReport,
139350
- result: "SKIPPED",
139351
- resultDescription: errorMessage
139352
- };
139353
- report.summary.contexts.skipped++;
139354
- testReport.contexts.push(contextReport);
139355
- continue;
139356
- }
139357
- let caps = getDriverCapabilities({
139358
- runnerDetails,
139359
- name: context.browser.name,
139360
- options: {
139361
- width: context.browser?.window?.width || 1200,
139362
- height: context.browser?.window?.height || 800,
139363
- headless: context.browser?.headless !== false
139364
- }
139365
- });
139366
- log(config, "debug", "CAPABILITIES:");
139367
- log(config, "debug", caps);
139368
- if (appiumPort === void 0) {
139369
- throw new Error("Driver requested but Appium was not started. isAppiumRequired(specs) and isDriverRequired(context) disagreed; this is a bug.");
139370
- }
139371
- try {
139372
- driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
139373
- } catch (error) {
139374
- try {
139375
- log(config, "warning", `Failed to start context '${context.browser?.name}' on '${platform}'. Retrying as headless.`);
139376
- context.browser.headless = true;
139377
- caps = getDriverCapabilities({
139378
- runnerDetails,
139379
- name: context.browser.name,
139380
- options: {
139381
- width: context.browser?.window?.width || 1200,
139382
- height: context.browser?.window?.height || 800,
139383
- headless: context.browser?.headless !== false
139384
- }
139385
- });
139386
- driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
139387
- } catch (error2) {
139388
- let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
139389
- if (context.browser?.name === "safari" || context.browser?.name === "webkit")
139390
- errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
139391
- log(config, "error", errorMessage);
139392
- if (!warmUpResults.has(combo))
139393
- warmUpResults.set(combo, "failed");
139394
- contextReport = {
139395
- ...contextReport,
139396
- result: "SKIPPED",
139397
- resultDescription: errorMessage
139398
- };
139399
- report.summary.contexts.skipped++;
139400
- testReport.contexts.push(contextReport);
139401
- continue;
139402
- }
139403
- }
139404
- if (!warmUpResults.has(combo))
139405
- warmUpResults.set(combo, "ok");
139406
- if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
139407
- await setViewportSize(context, driver);
139408
- } else if (context.browser?.window?.width || context.browser?.window?.height) {
139409
- const windowSize = await driver.getWindowSize();
139410
- await driver.setWindowSize(context.browser?.window?.width || windowSize.width, context.browser?.window?.height || windowSize.height);
139411
- }
139412
- }
139413
- let stepExecutionFailed = false;
139414
- for (let step of context.steps) {
139415
- if (!step.stepId)
139416
- step.stepId = (0, import_node_crypto5.randomUUID)();
139417
- log(config, "debug", `STEP:
139418
- ${JSON.stringify(step, null, 2)}`);
139419
- if (step.unsafe && runnerDetails.allowUnsafeSteps === false) {
139420
- log(config, "warning", `Skipping unsafe step: ${step.description} in test ${test.testId} context ${context.contextId}`);
139421
- const stepReport2 = {
139422
- ...step,
139423
- result: "SKIPPED",
139424
- resultDescription: "Skipped because unsafe steps aren't allowed."
139425
- };
139426
- contextReport.steps.push(stepReport2);
139427
- report.summary.steps.skipped++;
139428
- continue;
139429
- }
139430
- if (stepExecutionFailed) {
139431
- const stepReport2 = {
139432
- ...step,
139433
- result: "SKIPPED",
139434
- resultDescription: "Skipped due to previous failure in context."
139435
- };
139436
- contextReport.steps.push(stepReport2);
139437
- 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)
139438
139354
  continue;
139355
+ for (const stepReport of contextReport.steps) {
139356
+ report.summary.steps[stepReport.result.toLowerCase()]++;
139439
139357
  }
139440
- metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId].steps[step.stepId] = {};
139441
- const stepResult = await runStep({
139442
- config,
139443
- context,
139444
- step,
139445
- driver,
139446
- metaValues,
139447
- options: {
139448
- openApiDefinitions: context.openApi || []
139449
- }
139450
- });
139451
- log(config, "debug", `RESULT: ${stepResult.status}
139452
- ${JSON.stringify(stepResult, null, 2)}`);
139453
- stepResult.result = stepResult.status;
139454
- stepResult.resultDescription = stepResult.description;
139455
- delete stepResult.status;
139456
- delete stepResult.description;
139457
- const stepReport = {
139458
- ...step,
139459
- ...stepResult
139460
- };
139461
- contextReport.steps.push(stepReport);
139462
- report.summary.steps[stepReport.result.toLowerCase()]++;
139463
- if (stepReport.result === "FAIL") {
139464
- stepExecutionFailed = true;
139465
- }
139466
- }
139467
- if (config.recording) {
139468
- const stopRecordStep = {
139469
- stopRecord: true,
139470
- description: "Stopping recording",
139471
- stepId: (0, import_node_crypto5.randomUUID)()
139472
- };
139473
- const stepResult = await runStep({
139474
- config,
139475
- context,
139476
- step: stopRecordStep,
139477
- driver,
139478
- options: {
139479
- openApiDefinitions: context.openApi || []
139480
- }
139481
- });
139482
- stepResult.result = stepResult.status;
139483
- stepResult.resultDescription = stepResult.description;
139484
- delete stepResult.status;
139485
- delete stepResult.description;
139486
- const stepReport = {
139487
- ...stopRecordStep,
139488
- ...stepResult
139489
- };
139490
- contextReport.steps.push(stepReport);
139491
- report.summary.steps[stepReport.result.toLowerCase()]++;
139492
- }
139493
- let contextResult;
139494
- if (contextReport.steps.find((step) => step.result === "FAIL"))
139495
- contextResult = "FAIL";
139496
- else if (contextReport.steps.find((step) => step.result === "WARNING"))
139497
- contextResult = "WARNING";
139498
- else if (contextReport.steps.length === contextReport.steps.filter((step) => step.result === "SKIPPED").length)
139499
- contextResult = "SKIPPED";
139500
- else
139501
- contextResult = "PASS";
139502
- contextReport = { result: contextResult, ...contextReport };
139503
- testReport.contexts.push(contextReport);
139504
- report.summary.contexts[contextResult.toLowerCase()]++;
139505
- if (driverRequired) {
139506
- try {
139507
- await driver.deleteSession();
139508
- } catch (error) {
139509
- log(config, "error", `Failed to delete driver session: ${error.message}`);
139510
- }
139358
+ report.summary.contexts[contextReport.result.toLowerCase()]++;
139511
139359
  }
139360
+ testReport.result = rollUpResults(testReport.contexts.filter(Boolean));
139361
+ report.summary.tests[testReport.result.toLowerCase()]++;
139512
139362
  }
139513
- let testResult;
139514
- if (testReport.contexts.find((context) => context.result === "FAIL"))
139515
- testResult = "FAIL";
139516
- else if (testReport.contexts.find((context) => context.result === "WARNING"))
139517
- testResult = "WARNING";
139518
- else if (testReport.contexts.length === testReport.contexts.filter((context) => context.result === "SKIPPED").length)
139519
- testResult = "SKIPPED";
139520
- else
139521
- testResult = "PASS";
139522
- testReport = { result: testResult, ...testReport };
139523
- specReport.tests.push(testReport);
139524
- report.summary.tests[testResult.toLowerCase()]++;
139363
+ specReport.result = rollUpResults(specReport.tests);
139364
+ report.summary.specs[specReport.result.toLowerCase()]++;
139525
139365
  }
139526
- let specResult;
139527
- if (specReport.tests.find((test) => test.result === "FAIL"))
139528
- specResult = "FAIL";
139529
- else if (specReport.tests.find((test) => test.result === "WARNING"))
139530
- specResult = "WARNING";
139531
- else if (specReport.tests.length === specReport.tests.filter((test) => test.result === "SKIPPED").length)
139532
- specResult = "SKIPPED";
139533
- else
139534
- specResult = "PASS";
139535
- specReport = { result: specResult, ...specReport };
139536
- report.specs.push(specReport);
139537
- report.summary.specs[specResult.toLowerCase()]++;
139538
- }
139539
- if (appium) {
139540
- log(config, "debug", "Closing Appium server");
139541
- try {
139542
- (0, import_tree_kill.default)(appium.pid);
139543
- } 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
+ }
139544
139373
  }
139545
139374
  }
139546
139375
  const herettoConfigs = config?.integrations?.heretto || [];
@@ -139566,6 +139395,330 @@ ${JSON.stringify(stepResult, null, 2)}`);
139566
139395
  }
139567
139396
  return report;
139568
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
+ }
139569
139722
  async function runStep({ config = {}, context = {}, step, driver, metaValues = {}, options = {} }) {
139570
139723
  let actionResult;
139571
139724
  step = replaceEnvs(step);
@@ -139616,7 +139769,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
139616
139769
  step,
139617
139770
  driver
139618
139771
  });
139619
- config.recording = actionResult.recording;
139772
+ driver.state.recording = actionResult.recording ?? null;
139620
139773
  } else if (typeof step.runCode !== "undefined") {
139621
139774
  actionResult = await runCode({ config, step });
139622
139775
  } else if (typeof step.runShell !== "undefined") {
@@ -139641,7 +139794,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
139641
139794
  description: `Unknown step action: ${JSON.stringify(step)}`
139642
139795
  };
139643
139796
  }
139644
- if (config?.recording) {
139797
+ if (driver?.state?.recording) {
139645
139798
  const currentUrl = await driver.getUrl();
139646
139799
  if (currentUrl !== driver.state.url) {
139647
139800
  driver.state.url = currentUrl;
@@ -139663,6 +139816,33 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
139663
139816
  }
139664
139817
  return actionResult;
139665
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
+ }
139666
139846
  async function appiumIsReady(port, timeoutMs = 12e4) {
139667
139847
  let isReady = false;
139668
139848
  const start = Date.now();
@@ -139681,6 +139861,7 @@ async function appiumIsReady(port, timeoutMs = 12e4) {
139681
139861
  return isReady;
139682
139862
  }
139683
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;
139684
139865
  const wdio = await loadHeavyDep("webdriverio", { ctx });
139685
139866
  let lastError;
139686
139867
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -139697,11 +139878,11 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
139697
139878
  waitforTimeout: 12e4
139698
139879
  // 2 minutes
139699
139880
  });
139700
- driver.state = { url: "", x: null, y: null };
139881
+ driver.state = { url: "", x: null, y: null, recording: null };
139701
139882
  return driver;
139702
139883
  } catch (err) {
139703
139884
  lastError = err;
139704
- if (!/ECONNREFUSED/.test(String(err && err.message)))
139885
+ if (!TRANSIENT.test(String(err && err.message)))
139705
139886
  throw err;
139706
139887
  if (attempt < maxAttempts) {
139707
139888
  await new Promise((r) => setTimeout(r, 500 * attempt));