doc-detective 4.38.0 → 4.41.4
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.
- package/README.md +4 -4
- package/dist/agents/adapters/claude-code.d.ts.map +1 -1
- package/dist/agents/adapters/claude-code.js +1 -2
- package/dist/agents/adapters/claude-code.js.map +1 -1
- package/dist/agents/adapters/codex.d.ts.map +1 -1
- package/dist/agents/adapters/codex.js +3 -4
- package/dist/agents/adapters/codex.js.map +1 -1
- package/dist/agents/adapters/copilot-cli.d.ts.map +1 -1
- package/dist/agents/adapters/copilot-cli.js +1 -2
- package/dist/agents/adapters/copilot-cli.js.map +1 -1
- package/dist/agents/adapters/gemini-cli.d.ts.map +1 -1
- package/dist/agents/adapters/gemini-cli.js +1 -2
- package/dist/agents/adapters/gemini-cli.js.map +1 -1
- package/dist/agents/adapters/opencode.d.ts.map +1 -1
- package/dist/agents/adapters/opencode.js +1 -2
- package/dist/agents/adapters/opencode.js.map +1 -1
- package/dist/agents/adapters/qwen-code.d.ts.map +1 -1
- package/dist/agents/adapters/qwen-code.js +1 -2
- package/dist/agents/adapters/qwen-code.js.map +1 -1
- package/dist/agents/fetcher.d.ts.map +1 -1
- package/dist/agents/fetcher.js +1 -2
- package/dist/agents/fetcher.js.map +1 -1
- package/dist/common/src/ajv.d.ts +30 -0
- package/dist/common/src/ajv.d.ts.map +1 -0
- package/dist/common/src/ajv.js +131 -0
- package/dist/common/src/ajv.js.map +1 -0
- package/dist/common/src/detectTests.d.ts +2 -18
- package/dist/common/src/detectTests.d.ts.map +1 -1
- package/dist/common/src/detectTests.js +8 -77
- package/dist/common/src/detectTests.js.map +1 -1
- package/dist/common/src/schemas/referenced.d.ts +12 -0
- package/dist/common/src/schemas/referenced.d.ts.map +1 -0
- package/dist/common/src/schemas/referenced.js +10 -0
- package/dist/common/src/schemas/referenced.js.map +1 -0
- package/dist/common/src/schemas/schemas.json +2 -2
- package/dist/common/src/schemas/schemasReferenced.json +14871 -0
- package/dist/common/src/schemas/validators.cjs +509 -0
- package/dist/common/src/schemas/validatorsCheck.cjs +2 -0
- package/dist/common/src/textUtils.d.ts +25 -0
- package/dist/common/src/textUtils.d.ts.map +1 -0
- package/dist/common/src/textUtils.js +84 -0
- package/dist/common/src/textUtils.js.map +1 -0
- package/dist/common/src/types/generated/annotate_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/annotation_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/click_v3.d.ts +153 -0
- package/dist/common/src/types/generated/click_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/config_v3.d.ts +13753 -207
- package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +72 -0
- package/dist/common/src/types/generated/dragAndDrop_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/find_v3.d.ts +632 -0
- package/dist/common/src/types/generated/find_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/goTo_v3.d.ts +31 -0
- package/dist/common/src/types/generated/goTo_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/httpRequest_v3.d.ts +181 -0
- package/dist/common/src/types/generated/httpRequest_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/loadCookie_v3.d.ts +30 -0
- package/dist/common/src/types/generated/loadCookie_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/report_v3.d.ts +28234 -321
- package/dist/common/src/types/generated/report_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts +42281 -822
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/saveCookie_v3.d.ts +35 -0
- package/dist/common/src/types/generated/saveCookie_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/screenshot_v3.d.ts +45 -0
- package/dist/common/src/types/generated/screenshot_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/spec_v3.d.ts +28188 -275
- package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/startSurface_v3.d.ts +63 -1
- package/dist/common/src/types/generated/startSurface_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/step_v3.d.ts +5186 -523
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/test_v3.d.ts +13268 -3582
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/type_v3.d.ts +62 -0
- package/dist/common/src/types/generated/type_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/waitUntil_v3.d.ts +62 -0
- package/dist/common/src/types/generated/waitUntil_v3.d.ts.map +1 -1
- package/dist/common/src/validate.d.ts +1 -1
- package/dist/common/src/validate.d.ts.map +1 -1
- package/dist/common/src/validate.js +29 -75
- package/dist/common/src/validate.js.map +1 -1
- package/dist/core/appium.d.ts +3 -2
- package/dist/core/appium.d.ts.map +1 -1
- package/dist/core/appium.js +63 -89
- package/dist/core/appium.js.map +1 -1
- package/dist/core/appiumReuse.d.ts +43 -0
- package/dist/core/appiumReuse.d.ts.map +1 -0
- package/dist/core/appiumReuse.js +226 -0
- package/dist/core/appiumReuse.js.map +1 -0
- package/dist/core/config.d.ts +10 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +37 -7
- package/dist/core/config.js.map +1 -1
- package/dist/core/detectTests.d.ts.map +1 -1
- package/dist/core/detectTests.js +51 -17
- package/dist/core/detectTests.js.map +1 -1
- package/dist/core/files.d.ts.map +1 -1
- package/dist/core/files.js +3 -4
- package/dist/core/files.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +132 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/integrations/heretto.d.ts +1 -1
- package/dist/core/integrations/heretto.d.ts.map +1 -1
- package/dist/core/integrations/heretto.js +7 -6
- package/dist/core/integrations/heretto.js.map +1 -1
- package/dist/core/lazyYaml.d.ts +2 -0
- package/dist/core/lazyYaml.d.ts.map +1 -0
- package/dist/core/lazyYaml.js +13 -0
- package/dist/core/lazyYaml.js.map +1 -0
- package/dist/core/resolveTests.js +1 -1
- package/dist/core/resolveTests.js.map +1 -1
- package/dist/core/telem.d.ts +1 -1
- package/dist/core/telem.d.ts.map +1 -1
- package/dist/core/telem.js +2 -11
- package/dist/core/telem.js.map +1 -1
- package/dist/core/telemLite.d.ts +10 -0
- package/dist/core/telemLite.d.ts.map +1 -0
- package/dist/core/telemLite.js +101 -0
- package/dist/core/telemLite.js.map +1 -0
- package/dist/core/tests/androidEmulator.d.ts +67 -4
- package/dist/core/tests/androidEmulator.d.ts.map +1 -1
- package/dist/core/tests/androidEmulator.js +366 -37
- package/dist/core/tests/androidEmulator.js.map +1 -1
- package/dist/core/tests/appSurface.d.ts +25 -6
- package/dist/core/tests/appSurface.d.ts.map +1 -1
- package/dist/core/tests/appSurface.js +88 -52
- package/dist/core/tests/appSurface.js.map +1 -1
- package/dist/core/tests/browserSessions.d.ts +2 -1
- package/dist/core/tests/browserSessions.d.ts.map +1 -1
- package/dist/core/tests/browserSessions.js +7 -1
- package/dist/core/tests/browserSessions.js.map +1 -1
- package/dist/core/tests/checkLink.d.ts +1 -0
- package/dist/core/tests/checkLink.d.ts.map +1 -1
- package/dist/core/tests/checkLink.js +14 -5
- package/dist/core/tests/checkLink.js.map +1 -1
- package/dist/core/tests/closeSurface.d.ts.map +1 -1
- package/dist/core/tests/closeSurface.js +4 -3
- package/dist/core/tests/closeSurface.js.map +1 -1
- package/dist/core/tests/findStrategies.d.ts.map +1 -1
- package/dist/core/tests/findStrategies.js +153 -116
- package/dist/core/tests/findStrategies.js.map +1 -1
- package/dist/core/tests/geckoDriver.d.ts +16 -0
- package/dist/core/tests/geckoDriver.d.ts.map +1 -0
- package/dist/core/tests/geckoDriver.js +127 -0
- package/dist/core/tests/geckoDriver.js.map +1 -0
- package/dist/core/tests/goTo.d.ts.map +1 -1
- package/dist/core/tests/goTo.js +12 -33
- package/dist/core/tests/goTo.js.map +1 -1
- package/dist/core/tests/httpRequest.d.ts.map +1 -1
- package/dist/core/tests/httpRequest.js +4 -3
- package/dist/core/tests/httpRequest.js.map +1 -1
- package/dist/core/tests/iosSimulator.d.ts +4 -1
- package/dist/core/tests/iosSimulator.d.ts.map +1 -1
- package/dist/core/tests/iosSimulator.js +49 -1
- package/dist/core/tests/iosSimulator.js.map +1 -1
- package/dist/core/tests/mobileBrowser.d.ts +2 -1
- package/dist/core/tests/mobileBrowser.d.ts.map +1 -1
- package/dist/core/tests/mobileBrowser.js +5 -2
- package/dist/core/tests/mobileBrowser.js.map +1 -1
- package/dist/core/tests/runCode.d.ts +9 -1
- package/dist/core/tests/runCode.d.ts.map +1 -1
- package/dist/core/tests/runCode.js +58 -2
- package/dist/core/tests/runCode.js.map +1 -1
- package/dist/core/tests/saveScreenshot.d.ts +18 -2
- package/dist/core/tests/saveScreenshot.d.ts.map +1 -1
- package/dist/core/tests/saveScreenshot.js +159 -4
- package/dist/core/tests/saveScreenshot.js.map +1 -1
- package/dist/core/tests/startRecording.d.ts.map +1 -1
- package/dist/core/tests/startRecording.js +6 -4
- package/dist/core/tests/startRecording.js.map +1 -1
- package/dist/core/tests/startSurface.d.ts.map +1 -1
- package/dist/core/tests/startSurface.js +8 -2
- package/dist/core/tests/startSurface.js.map +1 -1
- package/dist/core/tests.d.ts +21 -7
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +372 -121
- package/dist/core/tests.js.map +1 -1
- package/dist/core/utils.d.ts +68 -3
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +551 -18
- package/dist/core/utils.js.map +1 -1
- package/dist/core/verification.d.ts +17 -0
- package/dist/core/verification.d.ts.map +1 -0
- package/dist/core/verification.js +850 -0
- package/dist/core/verification.js.map +1 -0
- package/dist/debug/appium.js +2 -2
- package/dist/debug/appium.js.map +1 -1
- package/dist/debug/findings.js +2 -2
- package/dist/debug/findings.js.map +1 -1
- package/dist/debug/tools.d.ts +8 -0
- package/dist/debug/tools.d.ts.map +1 -1
- package/dist/debug/tools.js +60 -22
- package/dist/debug/tools.js.map +1 -1
- package/dist/hints/context.d.ts.map +1 -1
- package/dist/hints/context.js +2 -2
- package/dist/hints/context.js.map +1 -1
- package/dist/index.cjs +25777 -4
- package/dist/reporters/htmlReporter.js +1 -1
- package/dist/runtime/androidInstaller.d.ts +45 -0
- package/dist/runtime/androidInstaller.d.ts.map +1 -1
- package/dist/runtime/androidInstaller.js +88 -5
- package/dist/runtime/androidInstaller.js.map +1 -1
- package/dist/runtime/browsers.d.ts +87 -7
- package/dist/runtime/browsers.d.ts.map +1 -1
- package/dist/runtime/browsers.js +308 -41
- package/dist/runtime/browsers.js.map +1 -1
- package/dist/runtime/cacheDir.d.ts +1 -1
- package/dist/runtime/cacheDir.d.ts.map +1 -1
- package/dist/runtime/cacheDir.js +5 -2
- package/dist/runtime/cacheDir.js.map +1 -1
- package/dist/runtime/heavyDeps.d.ts.map +1 -1
- package/dist/runtime/heavyDeps.js.map +1 -1
- package/dist/runtime/installCommand.d.ts.map +1 -1
- package/dist/runtime/installCommand.js +23 -0
- package/dist/runtime/installCommand.js.map +1 -1
- package/dist/runtime/loader.d.ts +13 -0
- package/dist/runtime/loader.d.ts.map +1 -1
- package/dist/runtime/loader.js +50 -0
- package/dist/runtime/loader.js.map +1 -1
- package/dist/runtime/wdaProducts.d.ts +12 -1
- package/dist/runtime/wdaProducts.d.ts.map +1 -1
- package/dist/runtime/wdaProducts.js +27 -2
- package/dist/runtime/wdaProducts.js.map +1 -1
- package/dist/runtime/windowsBash.d.ts +6 -1
- package/dist/runtime/windowsBash.d.ts.map +1 -1
- package/dist/runtime/windowsBash.js +45 -4
- package/dist/runtime/windowsBash.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +19 -5
- package/dist/utils.js.map +1 -1
- package/package.json +43 -41
package/dist/core/tests.js
CHANGED
|
@@ -8,15 +8,14 @@ import fs from "node:fs";
|
|
|
8
8
|
// optional package on disk — webdriverio is an optionalDependency that npm may
|
|
9
9
|
// skip on a CI runner, and a hard compile-time type reference would otherwise
|
|
10
10
|
// turn that skipped install into an intermittent build failure.
|
|
11
|
-
import { loadHeavyDep, resolveHeavyDepPath } from "../runtime/loader.js";
|
|
11
|
+
import { loadHeavyDep, prefetchHeavyDep, resolveHeavyDepPath } from "../runtime/loader.js";
|
|
12
12
|
import { resolveTheme } from "./annotations/model.js";
|
|
13
13
|
import { annotate, pruneExpired, renderLayer } from "./tests/annotate.js";
|
|
14
14
|
import { requiredBrowserAssets, ensureBrowserInstalled, } from "../runtime/browsers.js";
|
|
15
15
|
// Single source of truth for browser/driver-requiring step keys.
|
|
16
16
|
import { BROWSER_STEP_KEYS as driverActions, startSurfaceDescriptors, stepOpensBrowserSurface, stepTargetsProcessSurface, stepIsSurfacelessInteraction, testHasNonBrowserSurfaceSignal, } from "../runtime/browserStepKeys.js";
|
|
17
17
|
import os from "node:os";
|
|
18
|
-
import { log, logLevelEnabled, replaceEnvs, selectSpecsForRun, findFreePort, runConcurrent, runResourceAware, createResourceRegistry, rollUpResults, createAppiumPool, getRunOutputDir, runArchivesArtifacts, sanitizeFilesystemName, evaluateContextRequirements, isRetryableSessionError, classifyContextRetry,
|
|
19
|
-
import axios from "axios";
|
|
18
|
+
import { log, logLevelEnabled, replaceEnvs, selectSpecsForRun, findFreePort, runConcurrent, runResourceAware, createResourceRegistry, rollUpResults, createAppiumPool, getRunOutputDir, runArchivesArtifacts, sanitizeFilesystemName, evaluateContextRequirements, isRetryableSessionError, classifyContextRetry, guardBlankDocumentNavigation, applyBrowserSize, isViewportFloored, loadAxios, } from "./utils.js";
|
|
20
19
|
import { instantiateCursor } from "./tests/moveTo.js";
|
|
21
20
|
import { goTo } from "./tests/goTo.js";
|
|
22
21
|
import { findElement } from "./tests/findElement.js";
|
|
@@ -37,26 +36,28 @@ import { httpRequest } from "./tests/httpRequest.js";
|
|
|
37
36
|
import { clickElement } from "./tests/click.js";
|
|
38
37
|
import { runCode } from "./tests/runCode.js";
|
|
39
38
|
import { closeSurface } from "./tests/closeSurface.js";
|
|
40
|
-
import { createAppSessionState, appSurfacePreflight, isAppDriverRequired, stepTargetsAppSurface, teardownAppSession, APP_DRIVER_PLATFORMS, probeIosToolchain, } from "./tests/appSurface.js";
|
|
39
|
+
import { createAppSessionState, appSurfacePreflight, isAppDriverRequired, stepTargetsAppSurface, teardownAppSession, createAppServerPool, APP_DRIVER_PLATFORMS, probeIosToolchain, } from "./tests/appSurface.js";
|
|
41
40
|
import { startSurfaceStep } from "./tests/startSurface.js";
|
|
42
41
|
import { createActiveSurfaceTracker, } from "./tests/activeSurface.js";
|
|
43
42
|
import { isMobileTargetPlatform } from "./tests/mobilePlatform.js";
|
|
43
|
+
import { GECKODRIVER_EXECUTABLE_ARGS, applyDriverOptions, } from "./tests/geckoDriver.js";
|
|
44
44
|
import { mobileBrowserGate, buildMobileBrowserCapabilities, CHROMEDRIVER_AUTODOWNLOAD_ARGS, } from "./tests/mobileBrowser.js";
|
|
45
45
|
import { planWarmTasks, executeWarmTasks, raceBootInitiation, wrapInitiationEffects, RUNTIME_INSTALL_RESOURCE, } from "./warmPhase.js";
|
|
46
46
|
import { locateManagedWda } from "../runtime/wdaProducts.js";
|
|
47
47
|
import { getCacheDir } from "../runtime/cacheDir.js";
|
|
48
48
|
import { detectAndroidSdk } from "../runtime/androidSdk.js";
|
|
49
49
|
import { hostAbi, listInstalledSystemImages, installAndroid, } from "../runtime/androidInstaller.js";
|
|
50
|
-
import { buildAcquireDeviceDeps, checkEmulatorAcceleration, hostHasKvm, planDeviceAcquisition, planAndroidToolchain, normalizeDeviceDescriptor, acquireDevice, createDeviceRegistry, teardownDeviceRegistry, } from "./tests/androidEmulator.js";
|
|
51
|
-
import { buildAcquireSimulatorDeps, planSimulatorAcquisition, acquireSimulator, createSimulatorRegistry, teardownSimulatorRegistry, } from "./tests/iosSimulator.js";
|
|
50
|
+
import { buildAcquireDeviceDeps, findUia2Helpers, checkEmulatorAcceleration, hostHasKvm, planDeviceAcquisition, planAndroidToolchain, normalizeDeviceDescriptor, acquireDevice, createDeviceRegistry, teardownDeviceRegistry, shutdownEmulator, } from "./tests/androidEmulator.js";
|
|
51
|
+
import { buildAcquireSimulatorDeps, planSimulatorAcquisition, acquireSimulator, createSimulatorRegistry, teardownSimulatorRegistry, iosMajorOfRuntime, } from "./tests/iosSimulator.js";
|
|
52
52
|
import { runBrowserScript } from "./tests/runBrowserScript.js";
|
|
53
53
|
import { dragAndDropElement } from "./tests/dragAndDrop.js";
|
|
54
54
|
import { createSessionRegistry, registerSession, activeDriver, sweepSessions, } from "./tests/browserSessions.js";
|
|
55
55
|
import path from "node:path";
|
|
56
56
|
import { spawn } from "node:child_process";
|
|
57
57
|
import { randomUUID } from "node:crypto";
|
|
58
|
-
import { setAppiumHome } from "./appium.js";
|
|
59
|
-
import {
|
|
58
|
+
import { setAppiumHome, appiumHomeForDriverPath, useDriversArgs, browserPoolDriverPackages, } from "./appium.js";
|
|
59
|
+
import { reuseAppiumEnabled, appiumServerKey, createServerCache, takeServer, handBackServer, probeStatus, listSessions, allowSessionDiscovery, APPIUM_WATCHDOG_IMPORT, } from "./appiumReuse.js";
|
|
60
|
+
import { contentHash } from "../common/src/textUtils.js";
|
|
60
61
|
import { resolveExpression } from "./expressions.js";
|
|
61
62
|
import { evaluateCustomAssertions, evaluateGuard, guardReferencesSteps, customAssertionsReferenceSteps, resolveStepRouting, resolveTestRouting, computeRetryDelay, buildConditionContext, } from "./routing.js";
|
|
62
63
|
import { getEnvironment, getAvailableApps, clearAppCache, resolveConcurrentRunners, } from "./config.js";
|
|
@@ -65,8 +66,26 @@ import http from "node:http";
|
|
|
65
66
|
import https from "node:https";
|
|
66
67
|
import { fileURLToPath } from "node:url";
|
|
67
68
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
68
|
-
|
|
69
|
-
//
|
|
69
|
+
// Idle browser-pool Appium servers kept between runs in this process, only
|
|
70
|
+
// when DOC_DETECTIVE_REUSE_APPIUM is set (ADR 01125). Empty otherwise.
|
|
71
|
+
// killTree is a hoisted function declaration, called only when a server dies.
|
|
72
|
+
// Background kill failures log at debug under the last reusing run's config.
|
|
73
|
+
// Used only for that log line, so last-writer-wins across concurrent runs is fine.
|
|
74
|
+
// Only logLevel is read (by log()).
|
|
75
|
+
let lastReuseConfig;
|
|
76
|
+
const appiumServerCache = createServerCache({
|
|
77
|
+
kill: (server) => killTree(server.process?.pid),
|
|
78
|
+
onKillError: (server, err) => log(lastReuseConfig ?? {}, "debug", `Failed to close idle Appium server on port ${server.port}: ${err?.message ?? err}`),
|
|
79
|
+
idleMs: 60000,
|
|
80
|
+
maxIdle: 4,
|
|
81
|
+
});
|
|
82
|
+
// The runner's appiumIsReady call sites poll at this interval. A refused
|
|
83
|
+
// /status on loopback fails in about a millisecond, so a tighter poll costs
|
|
84
|
+
// nothing and trims the lag between Appium coming up and the run noticing
|
|
85
|
+
// (ADR 01124). appiumIsReady's own 250ms default stays for callers that don't
|
|
86
|
+
// pass `pollMs`.
|
|
87
|
+
const APPIUM_READY_POLL_MS = 50;
|
|
88
|
+
export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, withDriverPort, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, resolveRetryPolicy, runContextWithRetries, shouldRepairBeforeFallback, isSupportedContext, contextRequirementsSkipMessage, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, killTree, appiumServerCache, jobDisplayResources, buildWarmPlanDeps, warmBrowserInstall, prefetchMobileChromedriver, appiumIsReady, APPIUM_READY_POLL_MS, driverStart, };
|
|
70
89
|
// Browser names getDriverCapabilities knows how to build caps for. `safari` is
|
|
71
90
|
// rewritten to `webkit` during context resolution, so both appear here.
|
|
72
91
|
const KNOWN_BROWSERS = ["firefox", "chrome", "safari", "webkit"];
|
|
@@ -212,6 +231,18 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
212
231
|
"appium:newCommandTimeout": 600, // 10 minutes
|
|
213
232
|
browserName: "MozillaFirefox",
|
|
214
233
|
"wdio:enforceWebDriverClassic": true, // Disable BiDi, use classic mode
|
|
234
|
+
// Pin the geckodriver Doc Detective manages. appium-geckodriver
|
|
235
|
+
// resolves its binary from this capability or `which geckodriver` and
|
|
236
|
+
// nothing else, so without it the managed install in the browsers cache
|
|
237
|
+
// (version-suffixed, off PATH) can never be used — Firefox then only
|
|
238
|
+
// works where some other geckodriver happens to be on PATH, which is
|
|
239
|
+
// why containers fail and GitHub-hosted runners don't. The pool's
|
|
240
|
+
// servers always carry GECKODRIVER_EXECUTABLE_ARGS, which is what makes
|
|
241
|
+
// this insecure-gated capability legal to send. Omitted when no path
|
|
242
|
+
// was resolved, leaving the PATH lookup as the Layer 4 fallback.
|
|
243
|
+
...(firefox.driver
|
|
244
|
+
? { "appium:geckodriverExecutable": firefox.driver }
|
|
245
|
+
: {}),
|
|
215
246
|
"moz:firefoxOptions": {
|
|
216
247
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
|
|
217
248
|
args,
|
|
@@ -309,17 +340,26 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
309
340
|
// the other's connection is REFUSED, surfacing later as a mid-session
|
|
310
341
|
// `ECONNREFUSED 127.0.0.1:9515` when a command proxies to the wrong/dead
|
|
311
342
|
// chromedriver (ADR 01039). Assigning a unique free port per session removes the
|
|
312
|
-
// collision. Gecko
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
343
|
+
// collision. Gecko races the same way, just less often: appium-geckodriver
|
|
344
|
+
// picks the lowest free port in 5200..5300 for `systemPort` but only reserves
|
|
345
|
+
// it once geckodriver binds, so two concurrent Appium servers can both pick
|
|
346
|
+
// 5200 and one proxies into the other's geckodriver session ("Got unexpected
|
|
347
|
+
// session id"; ADR 01105). Safari has no such port, so its caps (and every
|
|
348
|
+
// other engine's) stay byte-identical. An explicit port capability (a caller
|
|
349
|
+
// opting into a fixed port) is preserved.
|
|
350
|
+
const DRIVER_PORT_CAPABILITY = {
|
|
351
|
+
Chromium: "appium:chromedriverPort",
|
|
352
|
+
Gecko: "appium:systemPort",
|
|
353
|
+
};
|
|
354
|
+
// The port capability these caps still need, or undefined when the engine has
|
|
355
|
+
// none or the caller already pinned one.
|
|
356
|
+
function driverPortCapability(capabilities) {
|
|
357
|
+
const key = DRIVER_PORT_CAPABILITY[capabilities?.["appium:automationName"]];
|
|
358
|
+
return key && capabilities[key] === undefined ? key : undefined;
|
|
359
|
+
}
|
|
360
|
+
function withDriverPort(capabilities, port) {
|
|
361
|
+
const key = driverPortCapability(capabilities);
|
|
362
|
+
return key ? { ...capabilities, [key]: port } : capabilities;
|
|
323
363
|
}
|
|
324
364
|
function isDriverRequired({ test }) {
|
|
325
365
|
let driverRequired = false;
|
|
@@ -495,6 +535,23 @@ function isBrowserRequired({ test }) {
|
|
|
495
535
|
// (the goTo-opener sibling); app/process descriptors don't.
|
|
496
536
|
stepOpensBrowserSurface(step)));
|
|
497
537
|
}
|
|
538
|
+
// Whether any context in `specs` could read the available-apps list: it names a
|
|
539
|
+
// browser, targets a mobile platform, or has a step that drives a browser or
|
|
540
|
+
// opens/closes a surface. A deliberate superset of every availableApps reader in
|
|
541
|
+
// this file, so a run where it's false never consults the list and can skip
|
|
542
|
+
// detection (ADR 01133). Falls back to the test's steps only when a context has
|
|
543
|
+
// no steps array; an explicit `steps: []` is a context with nothing to run.
|
|
544
|
+
export function runNeedsAvailableApps(specs) {
|
|
545
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
546
|
+
const stepNeedsApps = (step) => Boolean(step) &&
|
|
547
|
+
typeof step === "object" &&
|
|
548
|
+
(step.startSurface !== undefined ||
|
|
549
|
+
step.closeSurface !== undefined ||
|
|
550
|
+
driverActions.some((key) => step[key] !== undefined));
|
|
551
|
+
return list(specs).some((spec) => list(spec?.tests).some((test) => list(test?.contexts).some((context) => Boolean(context?.browser) ||
|
|
552
|
+
isMobileTargetPlatform(context?.platform) ||
|
|
553
|
+
list(Array.isArray(context?.steps) ? context.steps : test?.steps).some(stepNeedsApps))));
|
|
554
|
+
}
|
|
498
555
|
// Size the browser Appium server pool: the number of concurrent runner jobs
|
|
499
556
|
// that will actually create a BROWSER session. App-only jobs are excluded —
|
|
500
557
|
// they provision their own per-context Appium server (homed where the native
|
|
@@ -508,6 +565,12 @@ export function browserJobCount(jobs) {
|
|
|
508
565
|
return jobs.filter((job) => isBrowserRequired({ test: job?.context }))
|
|
509
566
|
.length;
|
|
510
567
|
}
|
|
568
|
+
// Whether a job's context has a screenshot step: gates the sharp prefetch
|
|
569
|
+
// during Appium's boot (ADR 01135).
|
|
570
|
+
export function jobTakesScreenshot(job) {
|
|
571
|
+
const steps = job?.context?.steps;
|
|
572
|
+
return Array.isArray(steps) && steps.some((s) => s?.screenshot !== undefined);
|
|
573
|
+
}
|
|
511
574
|
// Evaluate a context's `requires` capability gate. Returns null when the gate
|
|
512
575
|
// is absent or fully met; otherwise a skip message naming every unmet
|
|
513
576
|
// requirement, so the context lands as SKIPPED (the same non-failing outcome
|
|
@@ -685,7 +748,7 @@ async function androidContextPreflight({ config, context, clog, }) {
|
|
|
685
748
|
};
|
|
686
749
|
}
|
|
687
750
|
}
|
|
688
|
-
const deviceDeps = buildAcquireDeviceDeps(sdk, abi, (m) => clog("debug", m));
|
|
751
|
+
const deviceDeps = buildAcquireDeviceDeps(sdk, abi, (m) => clog("debug", m), uia2HelpersResolver(config));
|
|
689
752
|
// Device plan: every device the test needs must resolve (reuse an existing
|
|
690
753
|
// AVD/emulator, or create one from an installed image + Java). A gap SKIPs.
|
|
691
754
|
const running = await deviceDeps.listRunning();
|
|
@@ -913,29 +976,6 @@ function driverSkipDiagnostic({ requestedName, platform, platformMatches, attemp
|
|
|
913
976
|
msg += ` A present-but-broken driver (for example a partially downloaded ${driverHint}) can cause this; reinstall the driver or its browser.`;
|
|
914
977
|
return msg;
|
|
915
978
|
}
|
|
916
|
-
// Realize a context's target viewport and return the size the page actually
|
|
917
|
-
// rendered. Prefers viewport emulation (exact size, no window floor) and falls
|
|
918
|
-
// back to window resizing, warning if the browser/OS floored the request. The
|
|
919
|
-
// `// Confirm viewport size` intent is now realized by realizeViewport's
|
|
920
|
-
// read-back.
|
|
921
|
-
async function setViewportSize(context, driver, config = {}) {
|
|
922
|
-
// Guard on POSITIVE dimensions (not truthiness): the schema doesn't floor
|
|
923
|
-
// these, so a 0/negative/NaN value must not enter the resize path — matching
|
|
924
|
-
// the startSurface browser descriptor's guard.
|
|
925
|
-
const vw = Number(context.browser?.viewport?.width);
|
|
926
|
-
const vh = Number(context.browser?.viewport?.height);
|
|
927
|
-
if (vw > 0 || vh > 0) {
|
|
928
|
-
const requested = {
|
|
929
|
-
...(vw > 0 ? { width: vw } : {}),
|
|
930
|
-
...(vh > 0 ? { height: vh } : {}),
|
|
931
|
-
};
|
|
932
|
-
// Attribute the warning so a multi-context run can tell which context's
|
|
933
|
-
// viewport was floored.
|
|
934
|
-
const label = `viewport for ${context.browser?.name ?? "browser"} on ${context.platform ?? "host"}`;
|
|
935
|
-
return realizeViewport(driver, requested, config, label);
|
|
936
|
-
}
|
|
937
|
-
return undefined;
|
|
938
|
-
}
|
|
939
979
|
async function allowUnsafeSteps({ config }) {
|
|
940
980
|
// If allowUnsafeSteps is set to true, return true
|
|
941
981
|
if (config.allowUnsafeSteps === true)
|
|
@@ -958,6 +998,7 @@ async function allowUnsafeSteps({ config }) {
|
|
|
958
998
|
}
|
|
959
999
|
// Run specifications via API.
|
|
960
1000
|
async function runViaApi({ resolvedTests, apiKey, config = {} }) {
|
|
1001
|
+
const axios = await loadAxios();
|
|
961
1002
|
// Apply specFilter / testFilter before sending. Without this the API run
|
|
962
1003
|
// path silently ignores --test / --spec, since the orchestration server
|
|
963
1004
|
// sees the full unfiltered payload.
|
|
@@ -1126,10 +1167,16 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1126
1167
|
specs: [],
|
|
1127
1168
|
};
|
|
1128
1169
|
}
|
|
1129
|
-
// Get runner details
|
|
1170
|
+
// Get runner details. Browser detection is skipped when no context can use
|
|
1171
|
+
// its result (ADR 01133); APPIUM_HOME is still set, as detection would have.
|
|
1172
|
+
const needsApps = runNeedsAvailableApps(specs);
|
|
1173
|
+
if (!needsApps) {
|
|
1174
|
+
log(config, "debug", "No context in this run uses a browser; skipping browser detection.");
|
|
1175
|
+
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
1176
|
+
}
|
|
1130
1177
|
const runnerDetails = {
|
|
1131
1178
|
environment: getEnvironment(),
|
|
1132
|
-
availableApps: await getAvailableApps({ config }),
|
|
1179
|
+
availableApps: needsApps ? await getAvailableApps({ config }) : [],
|
|
1133
1180
|
allowUnsafeSteps: await allowUnsafeSteps({ config }),
|
|
1134
1181
|
};
|
|
1135
1182
|
// Set initial shorthand values
|
|
@@ -1310,6 +1357,9 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1310
1357
|
description: test.description,
|
|
1311
1358
|
contentPath: test.contentPath,
|
|
1312
1359
|
detectSteps: test.detectSteps,
|
|
1360
|
+
// Resolved by position via applyBadgeAnchoredTests (ADR 01141), not by name.
|
|
1361
|
+
badge: test.badge,
|
|
1362
|
+
location: test.location,
|
|
1313
1363
|
contexts: new Array(test.contexts.length),
|
|
1314
1364
|
};
|
|
1315
1365
|
specReport.tests.push(testReport);
|
|
@@ -1436,15 +1486,16 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1436
1486
|
const xvfbProcesses = [];
|
|
1437
1487
|
const useXvfbDisplays = concurrency.xvfbContexts.length > 0;
|
|
1438
1488
|
let portToDisplay;
|
|
1489
|
+
// Set only when this run takes and hands back idle servers (ADR 01125).
|
|
1490
|
+
// Xvfb runs never reuse: their displays die with the run.
|
|
1491
|
+
let reuseKey;
|
|
1439
1492
|
if (browserPoolJobCount > 0) {
|
|
1440
1493
|
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
1441
|
-
// Resolve appium's actual JS entrypoint via `require.resolve` (
|
|
1442
|
-
//
|
|
1443
|
-
// `node <entry>`. This sidesteps every shell-injection trap at
|
|
1444
|
-
// `.cmd` shim, so no Windows-requires-shell:true; no `npx`, so no
|
|
1445
|
-
// lookup; no user-controlled paths in a shell-interpreted string.
|
|
1446
|
-
// for both `--omit=optional` users (appium in cache only) and default
|
|
1447
|
-
// installs (appium in shim).
|
|
1494
|
+
// Resolve appium's actual JS entrypoint via `require.resolve` (always
|
|
1495
|
+
// from the runtime cache, the shared APPIUM_HOME — ADR 01111) and invoke
|
|
1496
|
+
// it with `node <entry>`. This sidesteps every shell-injection trap at
|
|
1497
|
+
// once: no `.cmd` shim, so no Windows-requires-shell:true; no `npx`, so no
|
|
1498
|
+
// PATH lookup; no user-controlled paths in a shell-interpreted string.
|
|
1448
1499
|
const appiumEntry = resolveHeavyDepPath("appium", {
|
|
1449
1500
|
cacheDir: config?.cacheDir,
|
|
1450
1501
|
});
|
|
@@ -1465,16 +1516,54 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1465
1516
|
// await them together so the waits OVERLAP — total ≈ max(readiness)
|
|
1466
1517
|
// instead of the sum of serial waits.
|
|
1467
1518
|
const readinessWaits = [];
|
|
1519
|
+
// Load only the desktop browser drivers (ADR 01117): the shared home
|
|
1520
|
+
// also holds the mobile drivers, which roughly doubled startup.
|
|
1521
|
+
const poolDriverPackages = browserPoolDriverPackages(process.platform);
|
|
1522
|
+
const useDrivers = useDriversArgs(process.env.APPIUM_HOME, poolDriverPackages);
|
|
1523
|
+
// Every desktop pool server allows the geckodriver-path insecure
|
|
1524
|
+
// feature, not just the ones a Firefox context lands on: these servers
|
|
1525
|
+
// are shared across contexts and cross-browser fallback can route a
|
|
1526
|
+
// chrome-authored context to Firefox mid-run, so gating the flag on the
|
|
1527
|
+
// run's authored browsers would leave the capability illegal exactly
|
|
1528
|
+
// when the fallback needs it. See GECKODRIVER_EXECUTABLE_ARGS.
|
|
1529
|
+
let serverArgs = [...GECKODRIVER_EXECUTABLE_ARGS, ...useDrivers];
|
|
1530
|
+
if (reuseAppiumEnabled() && !useXvfbDisplays) {
|
|
1531
|
+
// Hand-back must be able to list leftover sessions.
|
|
1532
|
+
serverArgs = allowSessionDiscovery(serverArgs);
|
|
1533
|
+
lastReuseConfig = config;
|
|
1534
|
+
reuseKey = appiumServerKey({
|
|
1535
|
+
entry: appiumEntry,
|
|
1536
|
+
args: serverArgs,
|
|
1537
|
+
env: process.env,
|
|
1538
|
+
appiumHome: process.env.APPIUM_HOME,
|
|
1539
|
+
packages: ["appium", ...poolDriverPackages],
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1468
1542
|
for (let i = 0; i < serverCount; i++) {
|
|
1543
|
+
const reused = reuseKey
|
|
1544
|
+
? await takeServer(appiumServerCache, reuseKey, { isReady: probeStatus })
|
|
1545
|
+
: undefined;
|
|
1546
|
+
if (reused) {
|
|
1547
|
+
log(config, "debug", `Reusing idle Appium server on port ${reused.port}.`);
|
|
1548
|
+
// A spawn would prefetch webdriverio (ADR 01135); a reused server
|
|
1549
|
+
// skips the spawn, so start the import here instead.
|
|
1550
|
+
prefetchHeavyDep("webdriverio", { cacheDir: config?.cacheDir });
|
|
1551
|
+
appiumServers.push(reused);
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1469
1554
|
let display;
|
|
1470
1555
|
if (useXvfbDisplays) {
|
|
1471
1556
|
display = xvfbDisplay(i);
|
|
1472
1557
|
xvfbProcesses.push(await startXvfb(display));
|
|
1473
1558
|
log(config, "debug", `Started Xvfb on ${display} for recording.`);
|
|
1474
1559
|
}
|
|
1475
|
-
|
|
1560
|
+
// A reusable server exits once this process dies (ADR 01125).
|
|
1561
|
+
const server = await spawnAppiumServer(appiumEntry, config, display, undefined, serverArgs, reuseKey ? ["--import", APPIUM_WATCHDOG_IMPORT] : undefined);
|
|
1562
|
+
appiumServerCache.resetPort(server.port);
|
|
1476
1563
|
appiumServers.push(server);
|
|
1477
|
-
const wait = appiumIsReady(server.port
|
|
1564
|
+
const wait = appiumIsReady(server.port, undefined, {
|
|
1565
|
+
pollMs: APPIUM_READY_POLL_MS,
|
|
1566
|
+
});
|
|
1478
1567
|
// Attach a no-op catch so that once Promise.all rejects on the FIRST
|
|
1479
1568
|
// failing server, the other still-pending readiness rejections don't
|
|
1480
1569
|
// surface as unhandled promise rejections. Promise.all still sees the
|
|
@@ -1483,6 +1572,8 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1483
1572
|
wait.catch(() => { });
|
|
1484
1573
|
readinessWaits.push(wait);
|
|
1485
1574
|
}
|
|
1575
|
+
if (sizingJobs.some(jobTakesScreenshot))
|
|
1576
|
+
prefetchHeavyDep("sharp", { cacheDir: config?.cacheDir });
|
|
1486
1577
|
await Promise.all(readinessWaits);
|
|
1487
1578
|
for (const server of appiumServers) {
|
|
1488
1579
|
log(config, "debug", `Appium is ready on port ${server.port}.`);
|
|
@@ -1525,6 +1616,10 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1525
1616
|
// Swept in the `finally` below — only simulators Doc Detective booted are
|
|
1526
1617
|
// shut down (launch-ownership).
|
|
1527
1618
|
const simulatorRegistry = createSimulatorRegistry();
|
|
1619
|
+
// Run-level pool of iOS app Appium servers (ADR 01115): sequential iOS
|
|
1620
|
+
// contexts reuse one server instead of starting and killing their own.
|
|
1621
|
+
// Drained in the `finally` below.
|
|
1622
|
+
const appServerPool = createAppServerPool();
|
|
1528
1623
|
// One resource registry per run, shared by the warm phase, every phase's
|
|
1529
1624
|
// flat pool, AND the routed sequencer — so warm's cache-mutating tasks,
|
|
1530
1625
|
// flat-pool recordings, and routed-spec recordings all contend on the same
|
|
@@ -1572,7 +1667,7 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1572
1667
|
signalHandled = true;
|
|
1573
1668
|
(async () => {
|
|
1574
1669
|
await killAllRegistered();
|
|
1575
|
-
await Promise.all(appiumServers.map((server) => killTree(server.process?.pid)));
|
|
1670
|
+
await Promise.all([...appiumServers, ...appServerPool.drain()].map((server) => killTree(server.process?.pid)));
|
|
1576
1671
|
for (const xvfb of xvfbProcesses) {
|
|
1577
1672
|
try {
|
|
1578
1673
|
xvfb.kill();
|
|
@@ -1665,6 +1760,7 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1665
1760
|
processRegistry,
|
|
1666
1761
|
deviceRegistry,
|
|
1667
1762
|
simulatorRegistry,
|
|
1763
|
+
appServerPool,
|
|
1668
1764
|
logPrefix: limit > 1 ? `[${job.test.testId}/${job.context.contextId}]` : "",
|
|
1669
1765
|
});
|
|
1670
1766
|
}
|
|
@@ -1746,6 +1842,7 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1746
1842
|
processRegistry,
|
|
1747
1843
|
deviceRegistry,
|
|
1748
1844
|
simulatorRegistry,
|
|
1845
|
+
appServerPool,
|
|
1749
1846
|
platform,
|
|
1750
1847
|
markAutoRecord,
|
|
1751
1848
|
limit,
|
|
@@ -1806,10 +1903,35 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1806
1903
|
// (shells out to `taskkill /T /F` on Windows); firing it without
|
|
1807
1904
|
// awaiting let the run finish before the browser was really dead,
|
|
1808
1905
|
// orphaning it.
|
|
1809
|
-
|
|
1906
|
+
// With reuse on (ADR 01125), a server with no sessions left goes back to
|
|
1907
|
+
// the cache instead; one that still has a session is killed the same way.
|
|
1908
|
+
await Promise.all(appiumServers.map(async (server) => {
|
|
1909
|
+
if (reuseKey) {
|
|
1910
|
+
const outcome = await handBackServer(appiumServerCache, reuseKey, server, {
|
|
1911
|
+
listSessions,
|
|
1912
|
+
});
|
|
1913
|
+
log(config, "debug", outcome === "parked"
|
|
1914
|
+
? `Keeping idle Appium server on port ${server.port} for reuse.`
|
|
1915
|
+
: `Closed Appium server on port ${server.port}`);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1810
1918
|
log(config, "debug", `Closing Appium server on port ${server.port}`);
|
|
1811
1919
|
return killTree(server.process?.pid);
|
|
1812
1920
|
}));
|
|
1921
|
+
// Close the pooled iOS app servers (ADR 01115) through the same teardown a
|
|
1922
|
+
// per-context server gets, so the Windows orphan sweep still applies.
|
|
1923
|
+
for (const server of appServerPool.drain()) {
|
|
1924
|
+
log(config, "debug", `Closing app Appium server on port ${server.port}`);
|
|
1925
|
+
// A fresh state with no `release` sends teardown down the kill path.
|
|
1926
|
+
const owner = createAppSessionState();
|
|
1927
|
+
owner.server = { port: server.port, process: server.process };
|
|
1928
|
+
try {
|
|
1929
|
+
await teardownAppSession(owner, (pid) => killTree(pid));
|
|
1930
|
+
}
|
|
1931
|
+
catch (error) {
|
|
1932
|
+
log(config, "debug", `Couldn't close app Appium server: ${error?.message ?? error}`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1813
1935
|
// Tear down any Xvfb virtual displays started for recording.
|
|
1814
1936
|
for (const xvfb of xvfbProcesses) {
|
|
1815
1937
|
try {
|
|
@@ -1827,7 +1949,9 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1827
1949
|
if (deviceRegistry.size > 0) {
|
|
1828
1950
|
await teardownDeviceRegistry(deviceRegistry, async (entry) => {
|
|
1829
1951
|
log(config, "debug", `Shutting down emulator "${entry.name}" (${entry.udid}).`);
|
|
1830
|
-
|
|
1952
|
+
// Graceful, not a tree-kill: a hard kill can leave the AVD's disk
|
|
1953
|
+
// images (and the clean boot snapshot inside them) inconsistent.
|
|
1954
|
+
await shutdownEmulator(entry);
|
|
1831
1955
|
});
|
|
1832
1956
|
}
|
|
1833
1957
|
// Sweep the iOS simulator registry (native app phase A4): shut down only the
|
|
@@ -1908,7 +2032,7 @@ async function runSpecs({ resolvedTests }) {
|
|
|
1908
2032
|
* NEVER touches the summary — Phase-3 in runSpecs remains the sole tally site;
|
|
1909
2033
|
* here we only push reports.
|
|
1910
2034
|
*/
|
|
1911
|
-
async function runRoutedSpec({ spec, specReport, specGuardSkip, config, runnerDetails, appiumPool, portToDisplay, metaValues, installAttempts, warmUpResults, processRegistry, deviceRegistry, simulatorRegistry, platform, markAutoRecord, limit, resourceRegistry, exclusivityCtx, }) {
|
|
2035
|
+
async function runRoutedSpec({ spec, specReport, specGuardSkip, config, runnerDetails, appiumPool, portToDisplay, metaValues, installAttempts, warmUpResults, processRegistry, deviceRegistry, simulatorRegistry, appServerPool, platform, markAutoRecord, limit, resourceRegistry, exclusivityCtx, }) {
|
|
1912
2036
|
// Set once a `stop:spec` (or deferred `stop:run`) decision halts the spec; the
|
|
1913
2037
|
// remaining tests are recorded SKIPPED with the stop reason and not executed.
|
|
1914
2038
|
let stopRest = false;
|
|
@@ -1969,6 +2093,8 @@ async function runRoutedSpec({ spec, specReport, specGuardSkip, config, runnerDe
|
|
|
1969
2093
|
description: test.description,
|
|
1970
2094
|
contentPath: test.contentPath,
|
|
1971
2095
|
detectSteps: test.detectSteps,
|
|
2096
|
+
badge: test.badge,
|
|
2097
|
+
location: test.location,
|
|
1972
2098
|
contexts: new Array(test.contexts.length),
|
|
1973
2099
|
};
|
|
1974
2100
|
// Stamp the visit number when a backward goToTest re-ran this test (additive
|
|
@@ -2072,6 +2198,7 @@ async function runRoutedSpec({ spec, specReport, specGuardSkip, config, runnerDe
|
|
|
2072
2198
|
processRegistry,
|
|
2073
2199
|
deviceRegistry,
|
|
2074
2200
|
simulatorRegistry,
|
|
2201
|
+
appServerPool,
|
|
2075
2202
|
logPrefix: limit > 1 ? `[${job.test.testId}/${job.context.contextId}]` : "",
|
|
2076
2203
|
});
|
|
2077
2204
|
}
|
|
@@ -2363,6 +2490,20 @@ async function warmBrowserInstall({ browserName, config, runnerDetails, installA
|
|
|
2363
2490
|
note: `'${browserName}' has no installable assets`,
|
|
2364
2491
|
};
|
|
2365
2492
|
}
|
|
2493
|
+
// Resolves the UiAutomator2 driver's helper APKs (and its version) from
|
|
2494
|
+
// wherever the driver is installed, for baking into clean snapshots (ADR
|
|
2495
|
+
// 01108). Lazy: the warm-phase boot can start before the driver install ends.
|
|
2496
|
+
function uia2HelpersResolver(config) {
|
|
2497
|
+
return () => {
|
|
2498
|
+
const entry = resolveHeavyDepPath("appium-uiautomator2-driver", {
|
|
2499
|
+
cacheDir: config?.cacheDir,
|
|
2500
|
+
});
|
|
2501
|
+
const home = entry ? appiumHomeForDriverPath(entry) : null;
|
|
2502
|
+
return home
|
|
2503
|
+
? findUia2Helpers(path.join(home, "node_modules", "appium-uiautomator2-driver"))
|
|
2504
|
+
: null;
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2366
2507
|
/**
|
|
2367
2508
|
* Light per-run Android environment probe for warm tasks: SDK + emulator
|
|
2368
2509
|
* binary + acceleration (or a running emulator). Null means "not ready" —
|
|
@@ -2379,7 +2520,7 @@ async function resolveAndroidWarmEnv(config) {
|
|
|
2379
2520
|
const sdk = detectAndroidSdk({ cacheDir: config?.cacheDir });
|
|
2380
2521
|
if (!sdk?.emulator)
|
|
2381
2522
|
return null;
|
|
2382
|
-
const deviceDeps = buildAcquireDeviceDeps(sdk, abi, (m) => log(config, "debug", m));
|
|
2523
|
+
const deviceDeps = buildAcquireDeviceDeps(sdk, abi, (m) => log(config, "debug", m), uia2HelpersResolver(config));
|
|
2383
2524
|
const running = await deviceDeps.listRunning();
|
|
2384
2525
|
const capable = running.length > 0 || (await checkEmulatorAcceleration(sdk.emulator));
|
|
2385
2526
|
if (!capable)
|
|
@@ -2685,7 +2826,13 @@ async function prefetchMobileChromedriver({ config, desc, deviceRegistry, getAnd
|
|
|
2685
2826
|
APPIUM_HOME: pre.appiumHome,
|
|
2686
2827
|
ANDROID_HOME: env.sdkRoot,
|
|
2687
2828
|
ANDROID_SDK_ROOT: env.sdkRoot,
|
|
2688
|
-
},
|
|
2829
|
+
}, [
|
|
2830
|
+
...CHROMEDRIVER_AUTODOWNLOAD_ARGS,
|
|
2831
|
+
// Load only UiAutomator2 (ADR 01117).
|
|
2832
|
+
...useDriversArgs(pre.appiumHome, [
|
|
2833
|
+
APP_DRIVER_PLATFORMS.android.driverPackage,
|
|
2834
|
+
]),
|
|
2835
|
+
]);
|
|
2689
2836
|
driver = await startDriver(buildMobileBrowserCapabilities({
|
|
2690
2837
|
platform: "android",
|
|
2691
2838
|
udid: acquired.entry.udid,
|
|
@@ -3016,7 +3163,7 @@ async function captureAutoScreenshot({ config, driver, spec, test, context, step
|
|
|
3016
3163
|
* report or summary counters — the caller owns aggregation, which keeps this
|
|
3017
3164
|
* function safe to run concurrently with sibling contexts.
|
|
3018
3165
|
*/
|
|
3019
|
-
async function runContext({ config, spec, test, context, runnerDetails, appiumPool, portToDisplay, metaValues, installAttempts, warmUpResults, processRegistry, deviceRegistry, simulatorRegistry, logPrefix = "", }) {
|
|
3166
|
+
async function runContext({ config, spec, test, context, runnerDetails, appiumPool, portToDisplay, metaValues, installAttempts, warmUpResults, processRegistry, deviceRegistry, simulatorRegistry, appServerPool, logPrefix = "", }) {
|
|
3020
3167
|
const platform = runnerDetails.environment.platform;
|
|
3021
3168
|
// `let`, not `const`: an on-demand browser install below re-detects available
|
|
3022
3169
|
// apps and reassigns this snapshot.
|
|
@@ -3145,6 +3292,7 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
3145
3292
|
appSession.appiumHome = pre.appiumHome;
|
|
3146
3293
|
appSession.iosSimulatorRegistry = simulatorRegistry;
|
|
3147
3294
|
appSession.iosSimulatorDeps = pre.simulatorDeps;
|
|
3295
|
+
appSession.appServerPool = appServerPool;
|
|
3148
3296
|
gateBrowserName = pre.mobileWebBrowserName;
|
|
3149
3297
|
/* c8 ignore stop */
|
|
3150
3298
|
}
|
|
@@ -3372,15 +3520,33 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
3372
3520
|
// Chromedriver autodownload is an Appium insecure feature; opt in
|
|
3373
3521
|
// scoped to the uiautomator2 driver on this run-owned server so it can
|
|
3374
3522
|
// fetch the chromedriver matching the device's Chrome.
|
|
3375
|
-
const extraArgs =
|
|
3523
|
+
const extraArgs = [
|
|
3524
|
+
...(mobileTarget === "android" ? CHROMEDRIVER_AUTODOWNLOAD_ARGS : []),
|
|
3525
|
+
// Load only the device's driver (ADR 01117).
|
|
3526
|
+
...useDriversArgs(appSession.appiumHome, [
|
|
3527
|
+
(mobileTarget === "android"
|
|
3528
|
+
? APP_DRIVER_PLATFORMS.android
|
|
3529
|
+
: APP_DRIVER_PLATFORMS.ios).driverPackage,
|
|
3530
|
+
]),
|
|
3531
|
+
];
|
|
3376
3532
|
// Server start + device boot are environment work: any failure there
|
|
3377
3533
|
// (port pressure, an emulator that can't finish booting on this host,
|
|
3378
3534
|
// simctl trouble) is a gating SKIP with the reason named — never a
|
|
3379
3535
|
// FAIL — matching the mobile rule that every environment gap SKIPs.
|
|
3380
3536
|
let acquired;
|
|
3381
3537
|
try {
|
|
3382
|
-
const
|
|
3383
|
-
|
|
3538
|
+
const start = async () => startAppiumServer(appSession.appiumEntry, config, undefined, env, extraArgs);
|
|
3539
|
+
// iOS shares the run's pooled server for this launch environment
|
|
3540
|
+
// (ADR 01115); `release` hands it back at context teardown.
|
|
3541
|
+
const pool = appSession.appServerPool;
|
|
3542
|
+
const server = pool
|
|
3543
|
+
? await pool.acquire(JSON.stringify({ appiumEntry: appSession.appiumEntry, env, extraArgs }), start)
|
|
3544
|
+
: await start();
|
|
3545
|
+
appSession.server = {
|
|
3546
|
+
port: server.port,
|
|
3547
|
+
process: server.process,
|
|
3548
|
+
release: server.release,
|
|
3549
|
+
};
|
|
3384
3550
|
const desc = normalizeDeviceDescriptor({
|
|
3385
3551
|
contextDevice: context.device,
|
|
3386
3552
|
platform: mobileTarget,
|
|
@@ -3427,13 +3593,19 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
3427
3593
|
// The resolved cache root (not the raw config field, which is
|
|
3428
3594
|
// usually unset) — the chromedriver autodownload dir lives here.
|
|
3429
3595
|
cacheDir: getCacheDir({ cacheDir: config?.cacheDir }),
|
|
3596
|
+
// iOS: the simulator's version picks the WDA launch mode (ADR 01110).
|
|
3597
|
+
iosMajor: iosMajorOfRuntime(acquired.entry.runtime),
|
|
3430
3598
|
});
|
|
3431
3599
|
try {
|
|
3432
3600
|
driver = await driverStart(capabilities, appSession.server.port, 2, {
|
|
3433
3601
|
cacheDir: config?.cacheDir,
|
|
3602
|
+
log: (m) => clog("warning", m),
|
|
3434
3603
|
});
|
|
3435
3604
|
}
|
|
3436
3605
|
catch (error) {
|
|
3606
|
+
// A client-side timeout can abort while the server is still creating
|
|
3607
|
+
// the session, so keep a pooled server out of reuse (ADR 01115).
|
|
3608
|
+
appSession.sessionLeftOpen = true;
|
|
3437
3609
|
// A capable host that can't open the device browser is an
|
|
3438
3610
|
// environment gap (the absent-browser precedent): SKIP with the
|
|
3439
3611
|
// likely cause named, never FAIL.
|
|
@@ -3500,18 +3672,18 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
3500
3672
|
runnerDetails,
|
|
3501
3673
|
name: browserName,
|
|
3502
3674
|
options: {
|
|
3503
|
-
width: overrides?.size?.width ||
|
|
3504
|
-
context.browser?.window?.width ||
|
|
3505
|
-
1200,
|
|
3506
|
-
height: overrides?.size?.height ||
|
|
3507
|
-
context.browser?.window?.height ||
|
|
3508
|
-
800,
|
|
3509
3675
|
headless,
|
|
3510
3676
|
...recordOptions,
|
|
3511
3677
|
},
|
|
3512
3678
|
});
|
|
3513
|
-
|
|
3514
|
-
|
|
3679
|
+
// Merge the authored escape hatch, minus the capabilities that are
|
|
3680
|
+
// only legal because we opted the server in to an insecure feature.
|
|
3681
|
+
// A plain Object.assign here would let a spec-authored
|
|
3682
|
+
// `appium:geckodriverExecutable` overwrite the managed path and have
|
|
3683
|
+
// Appium spawn any local binary. See PROTECTED_CAPABILITIES.
|
|
3684
|
+
if (overrides?.driverOptions) {
|
|
3685
|
+
applyDriverOptions(caps, overrides.driverOptions, (message) => log(config, "warning", message));
|
|
3686
|
+
}
|
|
3515
3687
|
return caps;
|
|
3516
3688
|
};
|
|
3517
3689
|
const startFailure = () => {
|
|
@@ -3652,6 +3824,18 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
3652
3824
|
const res = await startDriverForBrowser(engine, overrides);
|
|
3653
3825
|
if (!res.ok)
|
|
3654
3826
|
throw new Error(res.error);
|
|
3827
|
+
// A later session takes the context's size unless its
|
|
3828
|
+
// startSurface descriptor sets `size`. A sizing failure leaves
|
|
3829
|
+
// the session usable at its default size, so warn, don't fail.
|
|
3830
|
+
try {
|
|
3831
|
+
await applyBrowserSize(res.driver, Number(overrides?.size?.width) > 0 ||
|
|
3832
|
+
Number(overrides?.size?.height) > 0
|
|
3833
|
+
? { window: overrides.size }
|
|
3834
|
+
: context.browser, config, `browser session for ${engine}`);
|
|
3835
|
+
}
|
|
3836
|
+
catch (error) {
|
|
3837
|
+
log(config, "warning", `Couldn't size the new ${engine} session: ${error?.message ?? error}`);
|
|
3838
|
+
}
|
|
3655
3839
|
return res.driver;
|
|
3656
3840
|
},
|
|
3657
3841
|
isNameTaken: (name) => !!processRegistry?.has(name),
|
|
@@ -3663,31 +3847,18 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
3663
3847
|
driver,
|
|
3664
3848
|
});
|
|
3665
3849
|
}
|
|
3666
|
-
//
|
|
3667
|
-
//
|
|
3668
|
-
//
|
|
3669
|
-
const
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
if (isViewportFloored({
|
|
3679
|
-
...(viewportW > 0 ? { width: viewportW } : {}),
|
|
3680
|
-
...(viewportH > 0 ? { height: viewportH } : {}),
|
|
3681
|
-
}, realized)) {
|
|
3682
|
-
contextReport.viewportFloored = true;
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
else if (context.browser?.window?.width ||
|
|
3686
|
-
context.browser?.window?.height) {
|
|
3687
|
-
// Get driver window size
|
|
3688
|
-
const windowSize = await driver.getWindowSize();
|
|
3689
|
-
// Resize window if necessary
|
|
3690
|
-
await driver.setWindowSize(context.browser?.window?.width || windowSize.width, context.browser?.window?.height || windowSize.height);
|
|
3850
|
+
// Size the session: the authored viewport, else the authored window,
|
|
3851
|
+
// else the default viewport (ADR 01144). The label attributes a floor
|
|
3852
|
+
// warning to this context in a multi-context run.
|
|
3853
|
+
const sizing = await applyBrowserSize(driver, context.browser, config, `viewport for ${context.browser?.name ?? "browser"} on ${context.platform ?? "host"}`);
|
|
3854
|
+
// Stamp the context REPORT (not the context object — the report is a
|
|
3855
|
+
// curated copy) when the browser floored an authored viewport. A
|
|
3856
|
+
// context-level viewport has no step output to carry the realized size,
|
|
3857
|
+
// so this is the only signal the post-run `useMobilePlatforms` hint can
|
|
3858
|
+
// read.
|
|
3859
|
+
if (sizing.target.kind === "viewport" &&
|
|
3860
|
+
isViewportFloored(sizing.target, sizing.realized)) {
|
|
3861
|
+
contextReport.viewportFloored = true;
|
|
3691
3862
|
}
|
|
3692
3863
|
}
|
|
3693
3864
|
// Effective autoScreenshot for this context (test > spec > config).
|
|
@@ -4129,7 +4300,9 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
4129
4300
|
// retry warning the wrapper already logs. The alive-but-unusable cases do,
|
|
4130
4301
|
// because "the session responded and we retried anyway" is otherwise
|
|
4131
4302
|
// surprising in a log.
|
|
4132
|
-
const retryReason = await classifyContextRetry(probeDrivers
|
|
4303
|
+
const retryReason = await classifyContextRetry(probeDrivers, {
|
|
4304
|
+
navigated: contextReport.steps.some((s) => s.goTo !== undefined && s.result !== "SKIPPED"),
|
|
4305
|
+
});
|
|
4133
4306
|
if (retryReason) {
|
|
4134
4307
|
sessionDiedMidRun = true;
|
|
4135
4308
|
if (retryReason === "page-broken") {
|
|
@@ -4190,8 +4363,11 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
4190
4363
|
// start, so the sweep covers it; sessions a closeSurface step already
|
|
4191
4364
|
// ended are gone from the registry). In a finally so an unexpected throw
|
|
4192
4365
|
// can't leak sessions while sibling contexts keep running.
|
|
4366
|
+
// A session left open (a failed delete) must keep a pooled iOS app server
|
|
4367
|
+
// from being reused: the mobile-web session runs on it (ADR 01115).
|
|
4368
|
+
let sessionsClosed = true;
|
|
4193
4369
|
if (browserSessions) {
|
|
4194
|
-
await sweepSessions(browserSessions);
|
|
4370
|
+
sessionsClosed = await sweepSessions(browserSessions);
|
|
4195
4371
|
}
|
|
4196
4372
|
else if (driver) {
|
|
4197
4373
|
// Registry creation is unconditional after a driver starts, so this
|
|
@@ -4200,9 +4376,12 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
4200
4376
|
await driver.deleteSession();
|
|
4201
4377
|
}
|
|
4202
4378
|
catch (error) {
|
|
4379
|
+
sessionsClosed = false;
|
|
4203
4380
|
clog("error", `Failed to delete driver session: ${error.message}`);
|
|
4204
4381
|
}
|
|
4205
4382
|
}
|
|
4383
|
+
if (appSession && !sessionsClosed)
|
|
4384
|
+
appSession.sessionLeftOpen = true;
|
|
4206
4385
|
// Tear down app surfaces: close every remaining app session (the driver
|
|
4207
4386
|
// terminates apps it launched) and stop the app Appium server. Tree-kill
|
|
4208
4387
|
// in callback form so the server process is actually gone before the
|
|
@@ -4613,10 +4792,32 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
4613
4792
|
/* best-effort */
|
|
4614
4793
|
}
|
|
4615
4794
|
}
|
|
4616
|
-
|
|
4617
|
-
|
|
4795
|
+
// Load only this platform's app driver (ADR 01117).
|
|
4796
|
+
const driverPackage = context?.platform
|
|
4797
|
+
? APP_DRIVER_PLATFORMS[context.platform]?.driverPackage
|
|
4798
|
+
: undefined;
|
|
4799
|
+
const extraArgs = driverPackage
|
|
4800
|
+
? useDriversArgs(appiumHome, [driverPackage])
|
|
4801
|
+
: [];
|
|
4802
|
+
const start = async () => {
|
|
4803
|
+
const server = await startAppiumServer(appiumEntry, config, undefined, env, extraArgs);
|
|
4804
|
+
return { port: server.port, process: server.process };
|
|
4805
|
+
};
|
|
4806
|
+
// iOS contexts share the run's pooled server for this exact launch
|
|
4807
|
+
// environment and argument list (ADR 01115); teardown releases it
|
|
4808
|
+
// via `release`. Deliberately a different key from the mobile-web
|
|
4809
|
+
// site: app servers also carry the ffmpeg PATH, so the two never
|
|
4810
|
+
// share a server.
|
|
4811
|
+
const pool = appSession?.appServerPool;
|
|
4812
|
+
if (pool) {
|
|
4813
|
+
return pool.acquire(JSON.stringify({ appiumEntry, env, extraArgs }), start);
|
|
4814
|
+
}
|
|
4815
|
+
return start();
|
|
4618
4816
|
},
|
|
4619
|
-
startDriver: (capabilities, port) => driverStart(capabilities, port, 2, {
|
|
4817
|
+
startDriver: (capabilities, port) => driverStart(capabilities, port, 2, {
|
|
4818
|
+
cacheDir: config?.cacheDir,
|
|
4819
|
+
log: (m) => log(config, "warning", m),
|
|
4820
|
+
}),
|
|
4620
4821
|
// Mobile device acquisition (boot/create-and-boot, or reuse), bound
|
|
4621
4822
|
// to the run-level registry stashed on the app session. Android uses
|
|
4622
4823
|
// the emulator layer; iOS uses the simctl simulator layer. Both
|
|
@@ -4783,7 +4984,9 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
4783
4984
|
async function spawnAppiumServer(appiumEntry, config, display, extraEnv,
|
|
4784
4985
|
// Extra CLI args for the server, e.g. the scoped `--allow-insecure`
|
|
4785
4986
|
// chromedriver-autodownload opt-in for android mobile-web sessions.
|
|
4786
|
-
extraArgs
|
|
4987
|
+
extraArgs,
|
|
4988
|
+
// Node args placed before the entry, e.g. the reuse watchdog (ADR 01125).
|
|
4989
|
+
nodeArgs) {
|
|
4787
4990
|
const port = await findFreePort();
|
|
4788
4991
|
log(config, "debug", `Starting Appium on port ${port}`);
|
|
4789
4992
|
// When a virtual display is supplied (Linux Xvfb recording), launch the
|
|
@@ -4798,7 +5001,15 @@ extraArgs) {
|
|
|
4798
5001
|
...(extraEnv ?? {}),
|
|
4799
5002
|
}
|
|
4800
5003
|
: process.env;
|
|
4801
|
-
const proc = spawn(process.execPath, [
|
|
5004
|
+
const proc = spawn(process.execPath, [
|
|
5005
|
+
...(nodeArgs ?? []),
|
|
5006
|
+
appiumEntry,
|
|
5007
|
+
"-a",
|
|
5008
|
+
"127.0.0.1",
|
|
5009
|
+
"-p",
|
|
5010
|
+
String(port),
|
|
5011
|
+
...(extraArgs ?? []),
|
|
5012
|
+
], {
|
|
4802
5013
|
windowsHide: true,
|
|
4803
5014
|
cwd: path.join(__dirname, "../.."),
|
|
4804
5015
|
env,
|
|
@@ -4808,6 +5019,8 @@ extraArgs) {
|
|
|
4808
5019
|
});
|
|
4809
5020
|
proc.stdout.on("data", () => { });
|
|
4810
5021
|
proc.stderr.on("data", () => { });
|
|
5022
|
+
// Overlap webdriverio's ~350 ms import with Appium's boot (ADR 01135).
|
|
5023
|
+
prefetchHeavyDep("webdriverio", { cacheDir: config?.cacheDir });
|
|
4811
5024
|
return { port, process: proc, display };
|
|
4812
5025
|
}
|
|
4813
5026
|
async function startAppiumServer(appiumEntry, config, display, extraEnv,
|
|
@@ -4816,7 +5029,9 @@ async function startAppiumServer(appiumEntry, config, display, extraEnv,
|
|
|
4816
5029
|
extraArgs) {
|
|
4817
5030
|
const server = await spawnAppiumServer(appiumEntry, config, display, extraEnv, extraArgs);
|
|
4818
5031
|
try {
|
|
4819
|
-
await appiumIsReady(server.port
|
|
5032
|
+
await appiumIsReady(server.port, undefined, {
|
|
5033
|
+
pollMs: APPIUM_READY_POLL_MS,
|
|
5034
|
+
});
|
|
4820
5035
|
}
|
|
4821
5036
|
catch (error) {
|
|
4822
5037
|
// appiumIsReady threw or timed out — the spawned child is still alive and
|
|
@@ -4841,13 +5056,14 @@ const STATUS_PROBE_TIMEOUT_MS = 10000;
|
|
|
4841
5056
|
// `probe`/`sleep` are injectable for hermetic unit tests; the overall timeout
|
|
4842
5057
|
// cap (default 120s) is unchanged.
|
|
4843
5058
|
async function appiumIsReady(port, timeoutMs = 120000, deps = {}) {
|
|
5059
|
+
const pollMs = deps.pollMs ?? 250;
|
|
4844
5060
|
const probe = deps.probe ??
|
|
4845
5061
|
(async (p) => {
|
|
4846
5062
|
try {
|
|
4847
5063
|
// Bound each probe: without a per-request timeout a hung /status
|
|
4848
5064
|
// response would block this await indefinitely, and the overall
|
|
4849
5065
|
// `timeoutMs` guard (checked only between probes) could never fire.
|
|
4850
|
-
const resp = await
|
|
5066
|
+
const resp = await (await loadAxios()).get(`http://127.0.0.1:${p}/status`, {
|
|
4851
5067
|
timeout: STATUS_PROBE_TIMEOUT_MS,
|
|
4852
5068
|
});
|
|
4853
5069
|
return resp.status === 200;
|
|
@@ -4864,7 +5080,7 @@ async function appiumIsReady(port, timeoutMs = 120000, deps = {}) {
|
|
|
4864
5080
|
if (Date.now() - start > timeoutMs) {
|
|
4865
5081
|
throw new Error(`Appium server on port ${port} failed to start within ${timeoutMs / 1000} seconds`);
|
|
4866
5082
|
}
|
|
4867
|
-
await sleep(
|
|
5083
|
+
await sleep(pollMs);
|
|
4868
5084
|
}
|
|
4869
5085
|
}
|
|
4870
5086
|
// Start the Appium driver specified in `capabilities`.
|
|
@@ -4888,19 +5104,23 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
|
4888
5104
|
let lastError;
|
|
4889
5105
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4890
5106
|
try {
|
|
4891
|
-
// Chromium sessions get a unique free
|
|
4892
|
-
// browser contexts never collide on chromedriver's fixed
|
|
4893
|
-
//
|
|
5107
|
+
// Chromium and Gecko sessions get a unique free driver port so
|
|
5108
|
+
// concurrent browser contexts never collide on chromedriver's fixed
|
|
5109
|
+
// default (9515) or geckodriver's lowest-free-in-range pick; see
|
|
5110
|
+
// withDriverPort (ADR 01039, 01105). A FRESH port is allocated per
|
|
4894
5111
|
// attempt so a retryable ECONNREFUSED — the very rebind-race the Appium
|
|
4895
5112
|
// path already retries — moves to a new free port instead of re-racing
|
|
4896
|
-
// the same one.
|
|
4897
|
-
//
|
|
4898
|
-
const
|
|
4899
|
-
capabilities
|
|
4900
|
-
const attemptCaps = needsChromedriverPort
|
|
4901
|
-
? withChromedriverPort(capabilities, await findFreePort())
|
|
5113
|
+
// the same one. Engines without a driver port skip the allocation and
|
|
5114
|
+
// keep a byte-identical wdio.remote payload.
|
|
5115
|
+
const attemptCaps = driverPortCapability(capabilities)
|
|
5116
|
+
? withDriverPort(capabilities, await findFreePort())
|
|
4902
5117
|
: capabilities;
|
|
4903
|
-
|
|
5118
|
+
// Every attempt is reported to the reuse cache: a failed or abandoned
|
|
5119
|
+
// start may still register a session on this server, so it must not be
|
|
5120
|
+
// handed to a later run (ADR 01125).
|
|
5121
|
+
const settle = appiumServerCache.sessionStarting(port);
|
|
5122
|
+
// Start from a resolved promise so a synchronous throw still settles.
|
|
5123
|
+
const driver = await Promise.resolve().then(() => (ctx.remote ?? wdio.remote)({
|
|
4904
5124
|
protocol: "http",
|
|
4905
5125
|
hostname: "127.0.0.1",
|
|
4906
5126
|
port,
|
|
@@ -4909,6 +5129,15 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
|
4909
5129
|
capabilities: attemptCaps,
|
|
4910
5130
|
connectionRetryTimeout: startupCeiling,
|
|
4911
5131
|
waitforTimeout: 120000, // 2 minutes
|
|
5132
|
+
// webdriverio 10's `$` throws when a selector matches several
|
|
5133
|
+
// elements. Keep the documented first-match behavior (ADR 01143).
|
|
5134
|
+
strictSelectors: false,
|
|
5135
|
+
})).then((d) => {
|
|
5136
|
+
settle(true);
|
|
5137
|
+
return d;
|
|
5138
|
+
}, (e) => {
|
|
5139
|
+
settle(false);
|
|
5140
|
+
throw e;
|
|
4912
5141
|
});
|
|
4913
5142
|
// Per-context mutable state. `recordings` lives here (not on config)
|
|
4914
5143
|
// so concurrent contexts can't clobber each other's recordings. It is a
|
|
@@ -4923,6 +5152,8 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
|
4923
5152
|
if (!isRetryableSessionError(String(err && err.message), startupCeiling))
|
|
4924
5153
|
throw err;
|
|
4925
5154
|
if (attempt < maxAttempts) {
|
|
5155
|
+
const reason = String(err?.message ?? err).split("\n")[0];
|
|
5156
|
+
ctx.log?.(`Session start attempt ${attempt} of ${maxAttempts} failed (${reason}); retrying.`);
|
|
4926
5157
|
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
4927
5158
|
}
|
|
4928
5159
|
}
|
|
@@ -5085,7 +5316,15 @@ async function getRunner(options = {}) {
|
|
|
5085
5316
|
if (!appiumEntry) {
|
|
5086
5317
|
throw new Error("appium is not installed. Run `doc-detective install runtime appium` to install it.");
|
|
5087
5318
|
}
|
|
5088
|
-
const appium = spawn(process.execPath, [
|
|
5319
|
+
const appium = spawn(process.execPath, [
|
|
5320
|
+
appiumEntry,
|
|
5321
|
+
"-a",
|
|
5322
|
+
"127.0.0.1",
|
|
5323
|
+
"-p",
|
|
5324
|
+
String(appiumPort),
|
|
5325
|
+
// Load only the desktop browser drivers (ADR 01117).
|
|
5326
|
+
...useDriversArgs(process.env.APPIUM_HOME, browserPoolDriverPackages(process.platform)),
|
|
5327
|
+
], {
|
|
5089
5328
|
windowsHide: true,
|
|
5090
5329
|
cwd: path.join(__dirname, "../.."),
|
|
5091
5330
|
});
|
|
@@ -5095,11 +5334,14 @@ async function getRunner(options = {}) {
|
|
|
5095
5334
|
appium.on("error", (err) => {
|
|
5096
5335
|
log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
|
|
5097
5336
|
});
|
|
5337
|
+
prefetchHeavyDep("webdriverio", { cacheDir: config?.cacheDir }); // ADR 01135
|
|
5098
5338
|
// Wait for Appium to be ready. Same kill-on-throw guard as in
|
|
5099
5339
|
// runSpecs above — without it, a startup timeout would leave an
|
|
5100
5340
|
// orphan Appium child holding the ephemeral port.
|
|
5101
5341
|
try {
|
|
5102
|
-
await appiumIsReady(appiumPort
|
|
5342
|
+
await appiumIsReady(appiumPort, undefined, {
|
|
5343
|
+
pollMs: APPIUM_READY_POLL_MS,
|
|
5344
|
+
});
|
|
5103
5345
|
}
|
|
5104
5346
|
catch (error) {
|
|
5105
5347
|
try {
|
|
@@ -5140,6 +5382,15 @@ async function getRunner(options = {}) {
|
|
|
5140
5382
|
throw new Error(`Failed to start Chrome runner: ${error.message}`);
|
|
5141
5383
|
}
|
|
5142
5384
|
}
|
|
5385
|
+
// A fresh Chromium session starts parked on its initial blank document
|
|
5386
|
+
// (`data:,`) and sometimes stays there even though `url()` resolved, so the
|
|
5387
|
+
// navigation silently didn't take (ADR 01084, ADR 01088). Steps run through
|
|
5388
|
+
// `runStep` are covered by goTo's guard, but the documented way to drive this
|
|
5389
|
+
// runner is `await runner.url(...)` straight through WebdriverIO, which was
|
|
5390
|
+
// not: the caller got a blank page, and the failure surfaced later as an empty
|
|
5391
|
+
// title or a missing element with nothing pointing at the navigation. Guard
|
|
5392
|
+
// the session's own `url` command so every caller gets the same verification.
|
|
5393
|
+
guardBlankDocumentNavigation(runner, config);
|
|
5143
5394
|
// Set window size
|
|
5144
5395
|
try {
|
|
5145
5396
|
await runner.setWindowSize(width, height);
|