doc-detective 4.25.0 → 4.26.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 (87) hide show
  1. package/dist/common/src/schemas/schemas.json +15966 -4090
  2. package/dist/common/src/types/generated/closeSurface_v3.d.ts +2 -2
  3. package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +1 -1
  4. package/dist/common/src/types/generated/goTo_v3.d.ts +1 -1
  5. package/dist/common/src/types/generated/record_v3.d.ts +1 -1
  6. package/dist/common/src/types/generated/runBrowserScript_v3.d.ts +1 -1
  7. package/dist/common/src/types/generated/screenshot_v3.d.ts +1 -1
  8. package/dist/common/src/types/generated/startSurface_v3.d.ts +317 -3
  9. package/dist/common/src/types/generated/startSurface_v3.d.ts.map +1 -1
  10. package/dist/common/src/types/generated/step_v3.d.ts +329 -15
  11. package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
  12. package/dist/common/src/types/generated/surface_v3.d.ts +1 -1
  13. package/dist/common/src/types/generated/swipe_v3.d.ts +2 -2
  14. package/dist/common/src/types/generated/test_v3.d.ts +669 -41
  15. package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
  16. package/dist/common/src/types/generated/type_v3.d.ts +1 -1
  17. package/dist/core/resolveTests.d.ts.map +1 -1
  18. package/dist/core/resolveTests.js +8 -1
  19. package/dist/core/resolveTests.js.map +1 -1
  20. package/dist/core/tests/appGestures.d.ts +7 -1
  21. package/dist/core/tests/appGestures.d.ts.map +1 -1
  22. package/dist/core/tests/appGestures.js +24 -4
  23. package/dist/core/tests/appGestures.js.map +1 -1
  24. package/dist/core/tests/appSurface.d.ts +13 -1
  25. package/dist/core/tests/appSurface.d.ts.map +1 -1
  26. package/dist/core/tests/appSurface.js +42 -10
  27. package/dist/core/tests/appSurface.js.map +1 -1
  28. package/dist/core/tests/appWindows.d.ts +45 -0
  29. package/dist/core/tests/appWindows.d.ts.map +1 -0
  30. package/dist/core/tests/appWindows.js +638 -0
  31. package/dist/core/tests/appWindows.js.map +1 -0
  32. package/dist/core/tests/browserSessions.d.ts +25 -3
  33. package/dist/core/tests/browserSessions.d.ts.map +1 -1
  34. package/dist/core/tests/browserSessions.js +63 -27
  35. package/dist/core/tests/browserSessions.js.map +1 -1
  36. package/dist/core/tests/browserSurface.d.ts.map +1 -1
  37. package/dist/core/tests/browserSurface.js +5 -2
  38. package/dist/core/tests/browserSurface.js.map +1 -1
  39. package/dist/core/tests/closeSurface.d.ts.map +1 -1
  40. package/dist/core/tests/closeSurface.js +25 -3
  41. package/dist/core/tests/closeSurface.js.map +1 -1
  42. package/dist/core/tests/ffmpegRecorder.d.ts +1 -7
  43. package/dist/core/tests/ffmpegRecorder.d.ts.map +1 -1
  44. package/dist/core/tests/ffmpegRecorder.js +5 -22
  45. package/dist/core/tests/ffmpegRecorder.js.map +1 -1
  46. package/dist/core/tests/findElement.d.ts.map +1 -1
  47. package/dist/core/tests/findElement.js +27 -5
  48. package/dist/core/tests/findElement.js.map +1 -1
  49. package/dist/core/tests/processSurface.d.ts +22 -0
  50. package/dist/core/tests/processSurface.d.ts.map +1 -0
  51. package/dist/core/tests/processSurface.js +105 -0
  52. package/dist/core/tests/processSurface.js.map +1 -0
  53. package/dist/core/tests/runShell.d.ts.map +1 -1
  54. package/dist/core/tests/runShell.js +23 -94
  55. package/dist/core/tests/runShell.js.map +1 -1
  56. package/dist/core/tests/saveScreenshot.d.ts.map +1 -1
  57. package/dist/core/tests/saveScreenshot.js +27 -6
  58. package/dist/core/tests/saveScreenshot.js.map +1 -1
  59. package/dist/core/tests/startRecording.d.ts.map +1 -1
  60. package/dist/core/tests/startRecording.js +29 -14
  61. package/dist/core/tests/startRecording.js.map +1 -1
  62. package/dist/core/tests/startSurface.d.ts +17 -0
  63. package/dist/core/tests/startSurface.d.ts.map +1 -0
  64. package/dist/core/tests/startSurface.js +275 -0
  65. package/dist/core/tests/startSurface.js.map +1 -0
  66. package/dist/core/tests/swipe.d.ts.map +1 -1
  67. package/dist/core/tests/swipe.js +30 -5
  68. package/dist/core/tests/swipe.js.map +1 -1
  69. package/dist/core/tests/typeKeys.d.ts.map +1 -1
  70. package/dist/core/tests/typeKeys.js +22 -0
  71. package/dist/core/tests/typeKeys.js.map +1 -1
  72. package/dist/core/tests.d.ts +3 -1
  73. package/dist/core/tests.d.ts.map +1 -1
  74. package/dist/core/tests.js +174 -42
  75. package/dist/core/tests.js.map +1 -1
  76. package/dist/core/utils.d.ts.map +1 -1
  77. package/dist/core/utils.js +10 -5
  78. package/dist/core/utils.js.map +1 -1
  79. package/dist/index.cjs +17519 -4763
  80. package/dist/runtime/browserStepKeys.d.ts +22 -0
  81. package/dist/runtime/browserStepKeys.d.ts.map +1 -1
  82. package/dist/runtime/browserStepKeys.js +31 -0
  83. package/dist/runtime/browserStepKeys.js.map +1 -1
  84. package/dist/runtime/inferRuntimeNeeds.d.ts.map +1 -1
  85. package/dist/runtime/inferRuntimeNeeds.js +23 -3
  86. package/dist/runtime/inferRuntimeNeeds.js.map +1 -1
  87. package/package.json +1 -1
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import { loadHeavyDep, resolveHeavyDepPath } from "../runtime/loader.js";
12
12
  import { requiredBrowserAssets, ensureBrowserInstalled, } from "../runtime/browsers.js";
13
13
  // Single source of truth for browser/driver-requiring step keys.
14
- import { BROWSER_STEP_KEYS as driverActions } from "../runtime/browserStepKeys.js";
14
+ import { BROWSER_STEP_KEYS as driverActions, startSurfaceDescriptors, stepOpensBrowserSurface, } from "../runtime/browserStepKeys.js";
15
15
  import os from "node:os";
16
16
  import { log, replaceEnvs, selectSpecsForRun, findFreePort, runConcurrent, runResourceAware, createResourceRegistry, rollUpResults, createAppiumPool, getRunOutputDir, runArchivesArtifacts, sanitizeFilesystemName, evaluateContextRequirements, isRetryableSessionError, } from "./utils.js";
17
17
  import axios from "axios";
@@ -34,7 +34,8 @@ import { httpRequest } from "./tests/httpRequest.js";
34
34
  import { clickElement } from "./tests/click.js";
35
35
  import { runCode } from "./tests/runCode.js";
36
36
  import { closeSurface } from "./tests/closeSurface.js";
37
- import { createAppSessionState, appSurfacePreflight, isAppDriverRequired, stepTargetsAppSurface, startAppSurface, teardownAppSession, } from "./tests/appSurface.js";
37
+ import { createAppSessionState, appSurfacePreflight, isAppDriverRequired, stepTargetsAppSurface, teardownAppSession, } from "./tests/appSurface.js";
38
+ import { startSurfaceStep } from "./tests/startSurface.js";
38
39
  import { isMobileTargetPlatform } from "./tests/mobilePlatform.js";
39
40
  import { mobileBrowserGate, buildMobileBrowserCapabilities, } from "./tests/mobileBrowser.js";
40
41
  import { getCacheDir } from "../runtime/cacheDir.js";
@@ -58,7 +59,7 @@ import http from "node:http";
58
59
  import https from "node:https";
59
60
  import { fileURLToPath } from "node:url";
60
61
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
61
- export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, shouldRepairBeforeFallback, isSupportedContext, contextRequirementsSkipMessage, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, killTree, jobDisplayResources, };
62
+ export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, withChromedriverPort, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, shouldRepairBeforeFallback, isSupportedContext, contextRequirementsSkipMessage, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, killTree, jobDisplayResources, };
62
63
  // exports.appiumStart = appiumStart;
63
64
  // exports.appiumIsReady = appiumIsReady;
64
65
  // exports.driverStart = driverStart;
@@ -270,6 +271,26 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
270
271
  }
271
272
  return capabilities;
272
273
  }
274
+ // Bind a Chromium (chromedriver-backed) session to a specific chromedriver
275
+ // port. appium-chromium-driver hands `chromedriverPort` straight to
276
+ // appium-chromedriver; when it is undefined, appium-chromedriver falls back to
277
+ // its fixed DEFAULT_PORT (9515). Two concurrent browser contexts (own Appium
278
+ // server each, from the pool) then both spawn chromedriver on 9515 — one binds,
279
+ // the other's connection is REFUSED, surfacing later as a mid-session
280
+ // `ECONNREFUSED 127.0.0.1:9515` when a command proxies to the wrong/dead
281
+ // chromedriver (ADR 01039). Assigning a unique free port per session removes the
282
+ // collision. Gecko/Safari are unaffected — geckodriver auto-selects a free
283
+ // `systemPort` from a range, and Safari has no such port — so this only touches
284
+ // Chromium caps, leaving every other engine's caps byte-identical. An explicit
285
+ // `appium:chromedriverPort` (a caller opting into a fixed port) is preserved.
286
+ function withChromedriverPort(capabilities, port) {
287
+ if (!capabilities ||
288
+ capabilities["appium:automationName"] !== "Chromium" ||
289
+ capabilities["appium:chromedriverPort"] !== undefined) {
290
+ return capabilities;
291
+ }
292
+ return { ...capabilities, "appium:chromedriverPort": port };
293
+ }
273
294
  function isDriverRequired({ test }) {
274
295
  let driverRequired = false;
275
296
  // The resolved shape doesn't guarantee `steps` — treat a stepless test or
@@ -280,9 +301,21 @@ function isDriverRequired({ test }) {
280
301
  if (typeof step[action] !== "undefined")
281
302
  driverRequired = true;
282
303
  });
304
+ // A startSurface browser descriptor opens a WebDriver session (Phase 6);
305
+ // app/process descriptors provision their own runtimes and don't count.
306
+ if (stepOpensBrowserSurface(step))
307
+ driverRequired = true;
283
308
  });
284
309
  return driverRequired;
285
310
  }
311
+ // Non-android platforms whose native app surface is driven by a shared,
312
+ // per-host Appium driver server (macOS Mac2, Windows NovaWindows, iOS/xcuitest
313
+ // simulator). Two concurrent sessions against one of these clobber the shared
314
+ // server, so app-driver contexts on these platforms serialize on the
315
+ // "native-app-driver" resource. Mirrors the non-android keys of
316
+ // APP_DRIVER_PLATFORMS in tests/appSurface.ts; android is excluded because it
317
+ // already has its own "android-emulator" bound.
318
+ const NATIVE_APP_DRIVER_PLATFORMS = ["mac", "windows", "ios"];
286
319
  // The exclusive resources a context job must hold to run safely under
287
320
  // concurrency. A shared-display ffmpeg recording holds "display" exclusively
288
321
  // (jobExclusiveResources). And once ANY such recording is in the run
@@ -310,14 +343,77 @@ function jobDisplayResources(job, ctx) {
310
343
  hasBrowserStep: isBrowserRequired({ test: job.context }),
311
344
  hasAppStep: isAppDriverRequired({ test: job.context }),
312
345
  }).action === "proceed";
313
- const extra = attemptsEmulator ? ["android-emulator"] : [];
314
- if (base.length || extra.length)
315
- return [...new Set([...base, ...extra])];
316
- if (ctx.runHasDisplayRecording &&
317
- isDriverRequired({ test: { steps: job.context?.steps } })) {
318
- return ["display"];
319
- }
320
- return [];
346
+ // Non-android native app-driver contexts share a per-host driver stack that
347
+ // two concurrent sessions clobber (a proxied step fails because the driver
348
+ // "process is not running (probably crashed)" / "Session does not exist" /
349
+ // ECONNREFUSED to WebDriverAgent on :8100). Serialize them against each other
350
+ // on one exclusive resource the non-android sibling of the android-emulator
351
+ // bound (extending ADR 01001 the way ADR 01025 did for emulators). Android
352
+ // already has its own bound above, so it's excluded here (never double-tagged).
353
+ //
354
+ // Which contexts contend depends on the platform:
355
+ // - macOS Mac2 / Windows: only contexts that drive a NATIVE APP contend —
356
+ // the driver launches the app under test, but a plain desktop browser
357
+ // (firefox/chrome) uses a separate browser session and must STILL
358
+ // parallelize. So these require isAppDriverRequired.
359
+ // - iOS: the single per-host iOS simulator + WebDriverAgent is shared by
360
+ // BOTH native-app (xcuitest) AND mobile-web (Safari-on-sim) contexts, so
361
+ // two of EITHER kind clobber each other. So an ios context contends
362
+ // whether or not it has an app step (mobile-web-ios failed the same way
363
+ // apps-ios did under concurrency).
364
+ // As with attemptsEmulator, the context must also clear the mobile browser
365
+ // gate, so a context that deterministically SKIPs/FAILs (mixed app+web on
366
+ // mobile, unsupported browser, device-fixed config) boots nothing and never
367
+ // needlessly serializes other jobs.
368
+ const platform = job.context?.platform;
369
+ const contendsForNativeDriver = platform === "ios"
370
+ ? true // any ios context boots the shared simulator (app OR web)
371
+ : isAppDriverRequired({ test: job.context }); // mac/windows: app only
372
+ // The mobileBrowserGate check only makes sense on MOBILE targets (ios/android):
373
+ // it SKIPs a context that deterministically won't boot the shared driver
374
+ // (mixed native-app + device-browser, unsupported/misconfigured mobile
375
+ // browser). On DESKTOP (mac/windows) there is no device browser to mix with, so
376
+ // the gate must NOT run: a desktop app context that also RECORDS carries
377
+ // `record`/`stopRecord` steps (BROWSER_STEP_KEYS) whose non-object payloads
378
+ // aren't app-targeting, so isBrowserRequired reports a "browser step" and the
379
+ // gate would wrongly SKIP — dropping the native-app-driver bound and letting a
380
+ // recording app context run concurrently with another native app context,
381
+ // clobbering the single per-host Mac2 / NovaWindows driver. Desktop app
382
+ // contexts always boot that driver, so they always contend. (ADR 01040.)
383
+ const isMobileTarget = platform === "ios" || platform === "android";
384
+ const gateProceeds = !isMobileTarget ||
385
+ mobileBrowserGate({
386
+ platform,
387
+ browser: job.context?.browser,
388
+ hasBrowserStep: isBrowserRequired({ test: job.context }),
389
+ hasAppStep: isAppDriverRequired({ test: job.context }),
390
+ }).action === "proceed";
391
+ const attemptsNativeAppDriver = platform !== "android" &&
392
+ NATIVE_APP_DRIVER_PLATFORMS.includes(platform) &&
393
+ contendsForNativeDriver &&
394
+ gateProceeds;
395
+ const extra = [
396
+ ...(attemptsEmulator ? ["android-emulator"] : []),
397
+ ...(attemptsNativeAppDriver ? ["native-app-driver"] : []),
398
+ ];
399
+ // A run-wide shared-display recording promotes every driver context onto the
400
+ // "display" resource so its ffmpeg capture doesn't include their windows.
401
+ // This composes with the native-app-driver bound above — a native app
402
+ // context in such a run holds BOTH (e.g. ["native-app-driver","display"]).
403
+ // Android emulator contexts are exempt: an emulator renders off the host
404
+ // display, so it neither pollutes a screen capture nor needs the display
405
+ // mutex (it stays on "android-emulator" only, as before).
406
+ const displayPromotion = !attemptsEmulator &&
407
+ ctx.runHasDisplayRecording &&
408
+ isDriverRequired({ test: { steps: job.context?.steps } })
409
+ ? ["display"]
410
+ : [];
411
+ // Order the union driver-bound first (native-app-driver / android-emulator),
412
+ // then the display resource — whether "display" arrived via `base` (this job's
413
+ // own ffmpeg recording) or `displayPromotion` (a recording elsewhere in the
414
+ // run). This keeps the canonical `["native-app-driver", "display"]` shape
415
+ // stable regardless of which path added the display bound.
416
+ return [...new Set([...extra, ...base, ...displayPromotion])];
321
417
  }
322
418
  // Check if context is supported by current platform and available apps
323
419
  function isSupportedContext({ context, apps, platform }) {
@@ -355,8 +451,11 @@ function isBrowserRequired({ test }) {
355
451
  if (!Array.isArray(test?.steps))
356
452
  return false;
357
453
  return test.steps.some((step) => !step?.__autoRecord &&
358
- driverActions.some((action) => typeof step[action] !== "undefined") &&
359
- !stepTargetsAppSurface(step));
454
+ ((driverActions.some((action) => typeof step[action] !== "undefined") &&
455
+ !stepTargetsAppSurface(step)) ||
456
+ // Phase 6: `startSurface: { browser: … }` opens a browser session
457
+ // (the goTo-opener sibling); app/process descriptors don't.
458
+ stepOpensBrowserSurface(step)));
360
459
  }
361
460
  // Size the browser Appium server pool: the number of concurrent runner jobs
362
461
  // that will actually create a BROWSER session. App-only jobs are excluded —
@@ -387,14 +486,19 @@ function contextRequirementsSkipMessage({ context, deps, }) {
387
486
  return null;
388
487
  return `Skipping context on '${context.platform}': unmet requirements — ${missing.join(", ")}.`;
389
488
  }
390
- // The device descriptors a test needs: each startSurface's `device` (undefined
391
- // when it omits one — the context default / auto device). At least one entry so
392
- // a test with no explicit device still validates the default device.
393
- function collectDeviceDescriptors(context) {
489
+ // The device descriptors a test needs: each APP startSurface descriptor's
490
+ // `device` (undefined when it omits one — the context default / auto device),
491
+ // across both the object form and the Phase 6 parallel array form. Browser /
492
+ // process descriptors boot no device and contribute nothing. At least one
493
+ // entry so a test with no explicit device still validates the default device.
494
+ // Exported for a focused unit test.
495
+ export function collectDeviceDescriptors(context) {
394
496
  const out = [];
395
497
  for (const step of context?.steps ?? []) {
396
- if (step?.startSurface)
397
- out.push(step.startSurface.device);
498
+ for (const d of startSurfaceDescriptors(step)) {
499
+ if (d && typeof d === "object" && typeof d.app === "string")
500
+ out.push(d.device);
501
+ }
398
502
  }
399
503
  return out.length ? out : [undefined];
400
504
  }
@@ -2865,18 +2969,34 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
2865
2969
  downloadDir: browserDownloadDir(context.contextId),
2866
2970
  };
2867
2971
  // Start a session for one engine, headed first then (on failure) headless.
2868
- const startDriverForBrowser = async (browserName) => {
2869
- const wantHeadless = context.browser?.headless !== false;
2870
- const buildCaps = (headless) => getDriverCapabilities({
2871
- runnerDetails,
2872
- name: browserName,
2873
- options: {
2874
- width: context.browser?.window?.width || 1200,
2875
- height: context.browser?.window?.height || 800,
2876
- headless,
2877
- ...recordOptions,
2878
- },
2879
- });
2972
+ // `overrides` carries a startSurface browser descriptor's launch knobs
2973
+ // (Phase 6): explicit headless wins over the context setting, `size`
2974
+ // wins over the context window dimensions, and `driverOptions` merges
2975
+ // into the computed capabilities last (the app branch's escape-hatch
2976
+ // precedent).
2977
+ const startDriverForBrowser = async (browserName, overrides) => {
2978
+ const wantHeadless = overrides?.headless !== undefined
2979
+ ? overrides.headless
2980
+ : context.browser?.headless !== false;
2981
+ const buildCaps = (headless) => {
2982
+ const caps = getDriverCapabilities({
2983
+ runnerDetails,
2984
+ name: browserName,
2985
+ options: {
2986
+ width: overrides?.size?.width ||
2987
+ context.browser?.window?.width ||
2988
+ 1200,
2989
+ height: overrides?.size?.height ||
2990
+ context.browser?.window?.height ||
2991
+ 800,
2992
+ headless,
2993
+ ...recordOptions,
2994
+ },
2995
+ });
2996
+ if (overrides?.driverOptions)
2997
+ Object.assign(caps, overrides.driverOptions);
2998
+ return caps;
2999
+ };
2880
3000
  const startFailure = () => {
2881
3001
  let error = `Failed to start context '${browserName}' on '${platform}'.`;
2882
3002
  if (browserName === "safari" || browserName === "webkit") {
@@ -3011,8 +3131,8 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
3011
3131
  // stamps driver.state.engine and back-links driver.state.sessionRegistry.
3012
3132
  if (driver) {
3013
3133
  browserSessions = createSessionRegistry({
3014
- open: async (engine) => {
3015
- const res = await startDriverForBrowser(engine);
3134
+ open: async (engine, overrides) => {
3135
+ const res = await startDriverForBrowser(engine, overrides);
3016
3136
  if (!res.ok)
3017
3137
  throw new Error(res.error);
3018
3138
  return res.driver;
@@ -3690,17 +3810,17 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
3690
3810
  actionResult = await closeSurface({ config: config, step: step, driver, processRegistry, appSession });
3691
3811
  }
3692
3812
  else if (typeof step.startSurface !== "undefined") {
3693
- if (!appSession) {
3694
- actionResult = {
3695
- status: "FAIL",
3696
- description: "startSurface ran without an app session; this is a runner bug (runContext must preflight app-driver tests).",
3697
- };
3698
- }
3699
- else {
3700
- actionResult = await startAppSurface({
3813
+ {
3814
+ // Multi-surface Phase 6: startSurfaceStep dispatches app / browser /
3815
+ // process descriptors (and the parallel array form). The app lane
3816
+ // FAILs per descriptor when no app session was preflighted; browser
3817
+ // descriptors ride the context's session registry via `driver`.
3818
+ actionResult = await startSurfaceStep({
3701
3819
  config: config,
3702
3820
  step: step,
3703
3821
  appSession,
3822
+ driver,
3823
+ processRegistry,
3704
3824
  platform: context?.platform ?? "",
3705
3825
  serverDeps: {
3706
3826
  startServer: async (appiumEntry, appiumHome) => {
@@ -3930,13 +4050,25 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
3930
4050
  let lastError;
3931
4051
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3932
4052
  try {
4053
+ // Chromium sessions get a unique free chromedriver port so concurrent
4054
+ // browser contexts never collide on chromedriver's fixed default (9515);
4055
+ // see withChromedriverPort (ADR 01039). A FRESH port is allocated per
4056
+ // attempt so a retryable ECONNREFUSED — the very rebind-race the Appium
4057
+ // path already retries — moves to a new free port instead of re-racing
4058
+ // the same one. Only Chromium caps need a port; every other engine skips
4059
+ // the allocation and keeps a byte-identical wdio.remote payload.
4060
+ const needsChromedriverPort = capabilities?.["appium:automationName"] === "Chromium" &&
4061
+ capabilities?.["appium:chromedriverPort"] === undefined;
4062
+ const attemptCaps = needsChromedriverPort
4063
+ ? withChromedriverPort(capabilities, await findFreePort())
4064
+ : capabilities;
3933
4065
  const driver = await wdio.remote({
3934
4066
  protocol: "http",
3935
4067
  hostname: "127.0.0.1",
3936
4068
  port,
3937
4069
  path: "/",
3938
4070
  logLevel: "error",
3939
- capabilities,
4071
+ capabilities: attemptCaps,
3940
4072
  connectionRetryTimeout: startupCeiling,
3941
4073
  waitforTimeout: 120000, // 2 minutes
3942
4074
  });