doc-detective 4.22.0 → 4.24.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.
- package/dist/common/src/schemas/schemas.json +43158 -10448
- package/dist/common/src/types/generated/config_v3.d.ts +1 -1
- package/dist/common/src/types/generated/context_v3.d.ts +1 -1
- package/dist/common/src/types/generated/report_v3.d.ts +1 -1
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts +2 -2
- package/dist/common/src/types/generated/spec_v3.d.ts +1 -1
- package/dist/common/src/types/generated/step_v3.d.ts +453 -1
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/swipe_v3.d.ts +288 -0
- package/dist/common/src/types/generated/swipe_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/swipe_v3.js +7 -0
- package/dist/common/src/types/generated/swipe_v3.js.map +1 -0
- package/dist/common/src/types/generated/test_v3.d.ts +1935 -967
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/core/appium.d.ts +2 -1
- package/dist/core/appium.d.ts.map +1 -1
- package/dist/core/appium.js +47 -13
- package/dist/core/appium.js.map +1 -1
- package/dist/core/ptyProbeWorker.d.ts +2 -0
- package/dist/core/ptyProbeWorker.d.ts.map +1 -0
- package/dist/core/ptyProbeWorker.js +61 -0
- package/dist/core/ptyProbeWorker.js.map +1 -0
- package/dist/core/ptyWatchdog.d.ts +77 -0
- package/dist/core/ptyWatchdog.d.ts.map +1 -0
- package/dist/core/ptyWatchdog.js +179 -0
- package/dist/core/ptyWatchdog.js.map +1 -0
- package/dist/core/resolveTests.d.ts.map +1 -1
- package/dist/core/resolveTests.js +16 -4
- package/dist/core/resolveTests.js.map +1 -1
- package/dist/core/tests/appGestures.d.ts +27 -0
- package/dist/core/tests/appGestures.d.ts.map +1 -0
- package/dist/core/tests/appGestures.js +325 -0
- package/dist/core/tests/appGestures.js.map +1 -0
- package/dist/core/tests/appSurface.d.ts +23 -3
- package/dist/core/tests/appSurface.d.ts.map +1 -1
- package/dist/core/tests/appSurface.js +166 -19
- package/dist/core/tests/appSurface.js.map +1 -1
- package/dist/core/tests/browserWait.d.ts +1 -1
- package/dist/core/tests/browserWait.d.ts.map +1 -1
- package/dist/core/tests/browserWait.js +108 -84
- package/dist/core/tests/browserWait.js.map +1 -1
- package/dist/core/tests/click.d.ts.map +1 -1
- package/dist/core/tests/click.js +12 -8
- package/dist/core/tests/click.js.map +1 -1
- package/dist/core/tests/findElement.d.ts.map +1 -1
- package/dist/core/tests/findElement.js +96 -13
- package/dist/core/tests/findElement.js.map +1 -1
- package/dist/core/tests/mobileBrowser.d.ts +46 -0
- package/dist/core/tests/mobileBrowser.d.ts.map +1 -0
- package/dist/core/tests/mobileBrowser.js +141 -0
- package/dist/core/tests/mobileBrowser.js.map +1 -0
- package/dist/core/tests/mobilePlatform.d.ts +1 -7
- package/dist/core/tests/mobilePlatform.d.ts.map +1 -1
- package/dist/core/tests/mobilePlatform.js +5 -27
- package/dist/core/tests/mobilePlatform.js.map +1 -1
- package/dist/core/tests/movement.d.ts +43 -0
- package/dist/core/tests/movement.d.ts.map +1 -0
- package/dist/core/tests/movement.js +92 -0
- package/dist/core/tests/movement.js.map +1 -0
- package/dist/core/tests/swipe.d.ts +8 -0
- package/dist/core/tests/swipe.d.ts.map +1 -0
- package/dist/core/tests/swipe.js +158 -0
- package/dist/core/tests/swipe.js.map +1 -0
- package/dist/core/tests/typeKeys.d.ts +8 -1
- package/dist/core/tests/typeKeys.d.ts.map +1 -1
- package/dist/core/tests/typeKeys.js +123 -23
- package/dist/core/tests/typeKeys.js.map +1 -1
- package/dist/core/tests.d.ts +7 -1
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +250 -74
- package/dist/core/tests.js.map +1 -1
- package/dist/core/utils.d.ts +2 -1
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +62 -4
- package/dist/core/utils.js.map +1 -1
- package/dist/hints/context.d.ts.map +1 -1
- package/dist/hints/context.js +4 -0
- package/dist/hints/context.js.map +1 -1
- package/dist/hints/types.d.ts +3 -4
- package/dist/hints/types.d.ts.map +1 -1
- package/dist/index.cjs +44005 -10132
- package/dist/reporters/htmlReporter.js +3 -2
- package/dist/reporters/htmlReporter.js.map +1 -1
- package/dist/runtime/androidInstaller.d.ts +27 -0
- package/dist/runtime/androidInstaller.d.ts.map +1 -1
- package/dist/runtime/androidInstaller.js +138 -2
- package/dist/runtime/androidInstaller.js.map +1 -1
- package/dist/runtime/browserStepKeys.d.ts +17 -1
- package/dist/runtime/browserStepKeys.d.ts.map +1 -1
- package/dist/runtime/browserStepKeys.js +25 -0
- package/dist/runtime/browserStepKeys.js.map +1 -1
- package/dist/runtime/heavyDeps.d.ts +25 -0
- package/dist/runtime/heavyDeps.d.ts.map +1 -1
- package/dist/runtime/heavyDeps.js +34 -0
- package/dist/runtime/heavyDeps.js.map +1 -1
- package/dist/runtime/inferRuntimeNeeds.js +8 -2
- package/dist/runtime/inferRuntimeNeeds.js.map +1 -1
- package/dist/runtime/installer.d.ts +9 -0
- package/dist/runtime/installer.d.ts.map +1 -1
- package/dist/runtime/installer.js +17 -1
- package/dist/runtime/installer.js.map +1 -1
- package/dist/runtime/loader.d.ts.map +1 -1
- package/dist/runtime/loader.js +85 -1
- package/dist/runtime/loader.js.map +1 -1
- package/package.json +1 -1
package/dist/core/tests.js
CHANGED
|
@@ -13,7 +13,7 @@ import { requiredBrowserAssets, ensureBrowserInstalled, } from "../runtime/brows
|
|
|
13
13
|
// Single source of truth for browser/driver-requiring step keys.
|
|
14
14
|
import { BROWSER_STEP_KEYS as driverActions } from "../runtime/browserStepKeys.js";
|
|
15
15
|
import os from "node:os";
|
|
16
|
-
import { log, replaceEnvs, selectSpecsForRun, findFreePort, runConcurrent, runResourceAware, createResourceRegistry, rollUpResults, createAppiumPool, getRunOutputDir, runArchivesArtifacts, sanitizeFilesystemName, evaluateContextRequirements, } from "./utils.js";
|
|
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";
|
|
18
18
|
import { instantiateCursor } from "./tests/moveTo.js";
|
|
19
19
|
import { goTo } from "./tests/goTo.js";
|
|
@@ -21,6 +21,7 @@ import { findElement } from "./tests/findElement.js";
|
|
|
21
21
|
import { runShell } from "./tests/runShell.js";
|
|
22
22
|
import { checkLink } from "./tests/checkLink.js";
|
|
23
23
|
import { typeKeys } from "./tests/typeKeys.js";
|
|
24
|
+
import { swipeSurface } from "./tests/swipe.js";
|
|
24
25
|
import { wait } from "./tests/wait.js";
|
|
25
26
|
import { saveScreenshot } from "./tests/saveScreenshot.js";
|
|
26
27
|
import { startRecording } from "./tests/startRecording.js";
|
|
@@ -34,7 +35,9 @@ import { clickElement } from "./tests/click.js";
|
|
|
34
35
|
import { runCode } from "./tests/runCode.js";
|
|
35
36
|
import { closeSurface } from "./tests/closeSurface.js";
|
|
36
37
|
import { createAppSessionState, appSurfacePreflight, isAppDriverRequired, stepTargetsAppSurface, startAppSurface, teardownAppSession, } from "./tests/appSurface.js";
|
|
37
|
-
import { isMobileTargetPlatform
|
|
38
|
+
import { isMobileTargetPlatform } from "./tests/mobilePlatform.js";
|
|
39
|
+
import { mobileBrowserGate, buildMobileBrowserCapabilities, } from "./tests/mobileBrowser.js";
|
|
40
|
+
import { getCacheDir } from "../runtime/cacheDir.js";
|
|
38
41
|
import { detectAndroidSdk } from "../runtime/androidSdk.js";
|
|
39
42
|
import { hostAbi, listInstalledSystemImages, installAndroid, } from "../runtime/androidInstaller.js";
|
|
40
43
|
import { buildAcquireDeviceDeps, checkEmulatorAcceleration, hostHasKvm, planDeviceAcquisition, planAndroidToolchain, normalizeDeviceDescriptor, acquireDevice, createDeviceRegistry, teardownDeviceRegistry, } from "./tests/androidEmulator.js";
|
|
@@ -55,7 +58,7 @@ import http from "node:http";
|
|
|
55
58
|
import https from "node:https";
|
|
56
59
|
import { fileURLToPath } from "node:url";
|
|
57
60
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
58
|
-
export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, shouldRepairBeforeFallback, isSupportedContext, contextRequirementsSkipMessage, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, killTree, };
|
|
61
|
+
export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, shouldRepairBeforeFallback, isSupportedContext, contextRequirementsSkipMessage, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, killTree, jobDisplayResources, };
|
|
59
62
|
// exports.appiumStart = appiumStart;
|
|
60
63
|
// exports.appiumIsReady = appiumIsReady;
|
|
61
64
|
// exports.driverStart = driverStart;
|
|
@@ -295,11 +298,18 @@ function jobDisplayResources(job, ctx) {
|
|
|
295
298
|
// emulator is GBs of RAM, so bound their concurrency to one at a time. This
|
|
296
299
|
// is exclusivity-as-bound on a single resource name (a counted semaphore is
|
|
297
300
|
// future work); it composes with any recording "display" resource above.
|
|
298
|
-
// Only android contexts that will actually attempt the emulator take it
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
+
// Only android contexts that will actually attempt the emulator take it.
|
|
302
|
+
// Native app contexts always do; a mobile-web context (phase A5) does when
|
|
303
|
+
// its browser gate proceeds — a context the gate deterministically SKIPs or
|
|
304
|
+
// FAILs (unsupported browser, mixed app+web, device-fixed config) never
|
|
305
|
+
// boots anything, so it must not needlessly serialize other jobs.
|
|
301
306
|
const attemptsEmulator = job.context?.platform === "android" &&
|
|
302
|
-
|
|
307
|
+
mobileBrowserGate({
|
|
308
|
+
platform: "android",
|
|
309
|
+
browser: job.context?.browser,
|
|
310
|
+
hasBrowserStep: isBrowserRequired({ test: job.context }),
|
|
311
|
+
hasAppStep: isAppDriverRequired({ test: job.context }),
|
|
312
|
+
}).action === "proceed";
|
|
303
313
|
const extra = attemptsEmulator ? ["android-emulator"] : [];
|
|
304
314
|
if (base.length || extra.length)
|
|
305
315
|
return [...new Set([...base, ...extra])];
|
|
@@ -399,19 +409,29 @@ function requiredAndroidOsVersions(context) {
|
|
|
399
409
|
}
|
|
400
410
|
// Android context preflight (native app phase A3b): host-capability probe →
|
|
401
411
|
// lazy toolchain install (when the run needs it) → per-device resolvability →
|
|
402
|
-
// UiAutomator2 driver install. Every gap is a gating SKIP (never
|
|
412
|
+
// UiAutomator2 driver install. Every environment gap is a gating SKIP (never
|
|
413
|
+
// FAIL); the one FAIL is authored device-fixed browser config (phase A5 gate),
|
|
414
|
+
// which is a contradiction to surface loudly, not a missing capability. The
|
|
403
415
|
// toolchain (SDK + system image) is NOT installed by default, but IS lazily
|
|
404
416
|
// installed — with a loud warning surfaced to the terminal AND the output
|
|
405
417
|
// report — when a run that reaches a capable host actually needs it. On ok,
|
|
406
418
|
// returns the Appium entry/home, SDK root, injected device-effect bundle, and
|
|
407
419
|
// any warnings to attach to the context report.
|
|
408
420
|
async function androidContextPreflight({ config, context, clog, }) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
// Mobile-web gate (phase A5) — before ANY toolchain work, so unsupported
|
|
422
|
+
// browsers, mixed app+web contexts, and device-fixed browser config land
|
|
423
|
+
// deterministically on every host without touching the SDK.
|
|
424
|
+
const gate = mobileBrowserGate({
|
|
425
|
+
platform: "android",
|
|
426
|
+
browser: context.browser,
|
|
427
|
+
hasBrowserStep: isBrowserRequired({ test: context }),
|
|
428
|
+
hasAppStep: isAppDriverRequired({ test: context }),
|
|
429
|
+
});
|
|
430
|
+
if (gate.action === "skip") {
|
|
431
|
+
return { ok: false, level: gate.level, reason: gate.reason };
|
|
432
|
+
}
|
|
433
|
+
if (gate.action === "fail") {
|
|
434
|
+
return { ok: false, level: "warning", reason: gate.reason, fail: true };
|
|
415
435
|
}
|
|
416
436
|
/* c8 ignore start */
|
|
417
437
|
// The rest is effectful (SDK detect/install, emulator probes) so it's
|
|
@@ -558,6 +578,7 @@ async function androidContextPreflight({ config, context, clog, }) {
|
|
|
558
578
|
sdkRoot: sdk.sdkRoot,
|
|
559
579
|
deviceDeps,
|
|
560
580
|
warnings,
|
|
581
|
+
mobileWebBrowserName: gate.browserName,
|
|
561
582
|
};
|
|
562
583
|
}
|
|
563
584
|
catch (error) {
|
|
@@ -569,19 +590,26 @@ async function androidContextPreflight({ config, context, clog, }) {
|
|
|
569
590
|
}
|
|
570
591
|
/* c8 ignore stop */
|
|
571
592
|
}
|
|
572
|
-
// iOS context preflight (native app phase A4
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
593
|
+
// iOS context preflight (native app phase A4 + mobile web phase A5): the
|
|
594
|
+
// mobile-browser gate decides first (support matrix / device-fixed config /
|
|
595
|
+
// mixed-context deferral — all pure, all pre-toolchain); then host
|
|
596
|
+
// capability/toolchain probes via appSurfacePreflight, then validation that
|
|
597
|
+
// the context's default simulator can be resolved (booted/created) via
|
|
598
|
+
// simctl. On success return the appium entry/home, the injected simctl effect
|
|
599
|
+
// bundle for the run's acquireSimulator closure, and the device browser (if
|
|
600
|
+
// any) to open a session for.
|
|
578
601
|
async function iosContextPreflight({ config, context, }) {
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
})
|
|
584
|
-
|
|
602
|
+
const gate = mobileBrowserGate({
|
|
603
|
+
platform: "ios",
|
|
604
|
+
browser: context.browser,
|
|
605
|
+
hasBrowserStep: isBrowserRequired({ test: context }),
|
|
606
|
+
hasAppStep: isAppDriverRequired({ test: context }),
|
|
607
|
+
});
|
|
608
|
+
if (gate.action === "skip") {
|
|
609
|
+
return { ok: false, level: gate.level, reason: gate.reason };
|
|
610
|
+
}
|
|
611
|
+
if (gate.action === "fail") {
|
|
612
|
+
return { ok: false, level: "warning", reason: gate.reason, fail: true };
|
|
585
613
|
}
|
|
586
614
|
const pre = await appSurfacePreflight({ config, platform: "ios" });
|
|
587
615
|
if (!pre.ok)
|
|
@@ -622,6 +650,7 @@ async function iosContextPreflight({ config, context, }) {
|
|
|
622
650
|
appiumEntry: pre.appiumEntry,
|
|
623
651
|
appiumHome: pre.appiumHome,
|
|
624
652
|
simulatorDeps,
|
|
653
|
+
mobileWebBrowserName: gate.browserName,
|
|
625
654
|
};
|
|
626
655
|
/* c8 ignore stop */
|
|
627
656
|
}
|
|
@@ -1911,6 +1940,14 @@ function selectWarmUpTargets(jobs, runnerDetails) {
|
|
|
1911
1940
|
// would defeat the warm-up/install de-racing the pre-pass exists for.
|
|
1912
1941
|
if (!context.platform)
|
|
1913
1942
|
context.platform = platform;
|
|
1943
|
+
// Mobile contexts (android/ios targets) never warm up a desktop engine:
|
|
1944
|
+
// their browser runs ON the device through the per-context app Appium
|
|
1945
|
+
// server (phase A5), so a desktop warm-up would launch the wrong browser
|
|
1946
|
+
// on the wrong machine — and must not write a desktop default browser
|
|
1947
|
+
// onto the context (runContext's mobile branch resolves the device
|
|
1948
|
+
// browser itself).
|
|
1949
|
+
if (isMobileTargetPlatform(context.platform))
|
|
1950
|
+
continue;
|
|
1914
1951
|
// Size and target the warm-up by isBrowserRequired (not isDriverRequired):
|
|
1915
1952
|
// app-only contexts run on their own per-context Appium server, so they
|
|
1916
1953
|
// must not pull a browser into the pre-pass or get a default browser
|
|
@@ -2391,8 +2428,13 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2391
2428
|
// If "browser" isn't defined but is required by the test, set it to the
|
|
2392
2429
|
// first available browser in the sequence of Firefox, Chrome, Safari.
|
|
2393
2430
|
// App-targeted steps don't count: they run on the app session, so an
|
|
2394
|
-
// app-only test never boots a browser it won't use.
|
|
2395
|
-
|
|
2431
|
+
// app-only test never boots a browser it won't use. Mobile contexts don't
|
|
2432
|
+
// count either: their browser is the device's (chrome/safari), resolved by
|
|
2433
|
+
// the mobile branch below — a desktop default here would be wrong on both
|
|
2434
|
+
// the engine and the machine.
|
|
2435
|
+
if (!context.browser &&
|
|
2436
|
+
isBrowserRequired({ test: context }) &&
|
|
2437
|
+
!isMobileTargetPlatform(context.platform)) {
|
|
2396
2438
|
context.browser = getDefaultBrowser({ runnerDetails });
|
|
2397
2439
|
}
|
|
2398
2440
|
// Set context report
|
|
@@ -2419,6 +2461,11 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2419
2461
|
// context that passes its preflight (phase A3b). Declared here so the mobile
|
|
2420
2462
|
// branch can set it and fall through to the shared step-execution path.
|
|
2421
2463
|
let appSession;
|
|
2464
|
+
// Mobile web (phase A5): set when the mobile preflight resolved a device
|
|
2465
|
+
// browser for this context. The try block below then opens the browser
|
|
2466
|
+
// session on the device (through the app session's Appium server) instead
|
|
2467
|
+
// of the desktop engine path.
|
|
2468
|
+
let mobileWebBrowserName;
|
|
2422
2469
|
const mobileTarget = isMobileTargetPlatform(context.platform);
|
|
2423
2470
|
if (mobileTarget) {
|
|
2424
2471
|
const requirementsSkip = contextRequirementsSkipMessage({ context });
|
|
@@ -2428,59 +2475,71 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2428
2475
|
contextReport.resultDescription = requirementsSkip;
|
|
2429
2476
|
return contextReport;
|
|
2430
2477
|
}
|
|
2478
|
+
// Both preflights run the mobile-browser gate first (support matrix,
|
|
2479
|
+
// device-fixed config, mixed app+web) — `fail: true` marks an authored
|
|
2480
|
+
// contradiction (FAIL loudly); everything else lands SKIPPED. Applied via
|
|
2481
|
+
// this shared closure so the two typed branches below stay narrowed to
|
|
2482
|
+
// their platform's ok-shape (no cross-platform union casts).
|
|
2483
|
+
const gateOutcome = (pre) => {
|
|
2484
|
+
clog(pre.fail ? "error" : pre.level, pre.reason);
|
|
2485
|
+
contextReport.result = pre.fail ? "FAIL" : "SKIPPED";
|
|
2486
|
+
contextReport.resultDescription = pre.reason;
|
|
2487
|
+
return contextReport;
|
|
2488
|
+
};
|
|
2489
|
+
let gateBrowserName;
|
|
2431
2490
|
if (mobileTarget === "android") {
|
|
2432
|
-
// Android (phase A3b):
|
|
2433
|
-
//
|
|
2434
|
-
//
|
|
2435
|
-
//
|
|
2436
|
-
// run the steps.
|
|
2491
|
+
// Android (phase A3b): SDK detection is lazy (probed only here, only
|
|
2492
|
+
// for android contexts), so a run that never targets android pays
|
|
2493
|
+
// nothing. On ok, prime the app session with the device layer and FALL
|
|
2494
|
+
// THROUGH to run the steps.
|
|
2437
2495
|
const pre = await androidContextPreflight({ config, context, clog });
|
|
2438
|
-
if (!pre.ok)
|
|
2439
|
-
|
|
2440
|
-
contextReport.result = "SKIPPED";
|
|
2441
|
-
contextReport.resultDescription = pre.reason;
|
|
2442
|
-
return contextReport;
|
|
2443
|
-
}
|
|
2496
|
+
if (!pre.ok)
|
|
2497
|
+
return gateOutcome(pre);
|
|
2444
2498
|
/* c8 ignore start */
|
|
2445
2499
|
// The ok path only runs on a host with a real SDK + emulator (CI legs).
|
|
2446
2500
|
appSession = createAppSessionState();
|
|
2447
2501
|
appSession.appiumEntry = pre.appiumEntry;
|
|
2448
2502
|
appSession.appiumHome = pre.appiumHome;
|
|
2449
|
-
appSession.defaultDevice = context.device;
|
|
2450
2503
|
appSession.androidSdkRoot = pre.sdkRoot;
|
|
2451
2504
|
appSession.androidDeviceRegistry = deviceRegistry;
|
|
2452
2505
|
appSession.androidDeviceDeps = pre.deviceDeps;
|
|
2453
|
-
// resolved device (name) joins the context report the way resolved
|
|
2454
|
-
// browser versions do; the concrete udid is known once a surface boots
|
|
2455
|
-
// it.
|
|
2456
|
-
contextReport.device = context.device ?? { platform: "android" };
|
|
2457
2506
|
// Surface any preflight warnings (e.g. a lazy toolchain install) in the
|
|
2458
2507
|
// output report — not just the terminal — so a run that quietly
|
|
2459
2508
|
// downloaded the multi-GB SDK is auditable after the fact.
|
|
2460
2509
|
if (pre.warnings.length)
|
|
2461
2510
|
contextReport.warnings = pre.warnings;
|
|
2511
|
+
gateBrowserName = pre.mobileWebBrowserName;
|
|
2462
2512
|
/* c8 ignore stop */
|
|
2463
2513
|
}
|
|
2464
2514
|
else {
|
|
2515
|
+
// iOS (phase A4): on ok, prime the app session with the simulator layer.
|
|
2465
2516
|
const pre = await iosContextPreflight({ config, context });
|
|
2466
|
-
if (!pre.ok)
|
|
2467
|
-
|
|
2468
|
-
contextReport.result = "SKIPPED";
|
|
2469
|
-
contextReport.resultDescription = pre.reason;
|
|
2470
|
-
return contextReport;
|
|
2471
|
-
}
|
|
2517
|
+
if (!pre.ok)
|
|
2518
|
+
return gateOutcome(pre);
|
|
2472
2519
|
/* c8 ignore start */
|
|
2473
|
-
// The ok path only runs on a capable macOS host (CI fixture legs)
|
|
2474
|
-
// the app session with the simulator layer and FALL THROUGH to run steps.
|
|
2520
|
+
// The ok path only runs on a capable macOS host (CI fixture legs).
|
|
2475
2521
|
appSession = createAppSessionState();
|
|
2476
2522
|
appSession.appiumEntry = pre.appiumEntry;
|
|
2477
2523
|
appSession.appiumHome = pre.appiumHome;
|
|
2478
|
-
appSession.defaultDevice = context.device;
|
|
2479
2524
|
appSession.iosSimulatorRegistry = simulatorRegistry;
|
|
2480
2525
|
appSession.iosSimulatorDeps = pre.simulatorDeps;
|
|
2481
|
-
|
|
2526
|
+
gateBrowserName = pre.mobileWebBrowserName;
|
|
2482
2527
|
/* c8 ignore stop */
|
|
2483
2528
|
}
|
|
2529
|
+
/* c8 ignore start */
|
|
2530
|
+
appSession.defaultDevice = context.device;
|
|
2531
|
+
// resolved device (name) joins the context report the way resolved
|
|
2532
|
+
// browser versions do; the concrete udid is known once a device boots.
|
|
2533
|
+
contextReport.device = context.device ?? { platform: mobileTarget };
|
|
2534
|
+
if (gateBrowserName) {
|
|
2535
|
+
// Mobile web: pin the context's browser to the device browser the gate
|
|
2536
|
+
// resolved (the authored one, or the platform default) so the report
|
|
2537
|
+
// and the session capabilities agree.
|
|
2538
|
+
mobileWebBrowserName = gateBrowserName;
|
|
2539
|
+
context.browser = { ...(context.browser ?? {}), name: mobileWebBrowserName };
|
|
2540
|
+
contextReport.browser = context.browser;
|
|
2541
|
+
}
|
|
2542
|
+
/* c8 ignore stop */
|
|
2484
2543
|
}
|
|
2485
2544
|
// `requires` capability gate: any unmet requirement skips the context with a
|
|
2486
2545
|
// message naming what's missing. Evaluated only on the platform the context
|
|
@@ -2609,8 +2668,10 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2609
2668
|
})
|
|
2610
2669
|
: [];
|
|
2611
2670
|
// A driver context with no startable engine is skipped with a diagnostic that
|
|
2612
|
-
// names the requested engine and the partial-download cause.
|
|
2613
|
-
|
|
2671
|
+
// names the requested engine and the partial-download cause. Mobile-web
|
|
2672
|
+
// contexts don't participate: their browser lives on the device, not in the
|
|
2673
|
+
// host's engine list (their session path is the mobile block below).
|
|
2674
|
+
if (driverRequired && !mobileWebBrowserName && candidateEngines.length === 0) {
|
|
2614
2675
|
const errorMessage = freshInstallRedetected
|
|
2615
2676
|
? freshInstallOutcome === "installed"
|
|
2616
2677
|
? `Skipping context '${requestedBrowserName}' on '${context.platform}': the missing browser dependency was installed but still could not be detected.`
|
|
@@ -2659,7 +2720,120 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2659
2720
|
"predicate disagreed; this is a bug.");
|
|
2660
2721
|
}
|
|
2661
2722
|
try {
|
|
2662
|
-
|
|
2723
|
+
/* c8 ignore start */
|
|
2724
|
+
// Mobile web (phase A5): only reachable on a capable host (the mobile
|
|
2725
|
+
// preflight gates everything else), so it's exercised by the
|
|
2726
|
+
// mobile-web fixture legs, not the unit suite.
|
|
2727
|
+
if (driverRequired && mobileWebBrowserName) {
|
|
2728
|
+
// The browser session lives ON the managed device, through the app
|
|
2729
|
+
// session's Appium server (homed where the mobile driver lives) — not
|
|
2730
|
+
// the desktop engine pool. Acquire/boot the context's default device,
|
|
2731
|
+
// then open one webdriver session with browserName set so Appium starts
|
|
2732
|
+
// it in a web context; goTo/find/click/screenshot then behave exactly
|
|
2733
|
+
// as on desktop.
|
|
2734
|
+
const env = {
|
|
2735
|
+
APPIUM_HOME: appSession.appiumHome,
|
|
2736
|
+
};
|
|
2737
|
+
if (appSession.androidSdkRoot) {
|
|
2738
|
+
env.ANDROID_HOME = appSession.androidSdkRoot;
|
|
2739
|
+
env.ANDROID_SDK_ROOT = appSession.androidSdkRoot;
|
|
2740
|
+
}
|
|
2741
|
+
// Chromedriver autodownload is an Appium insecure feature; opt in
|
|
2742
|
+
// scoped to the uiautomator2 driver on this run-owned server so it can
|
|
2743
|
+
// fetch the chromedriver matching the device's Chrome.
|
|
2744
|
+
const extraArgs = mobileTarget === "android"
|
|
2745
|
+
? ["--allow-insecure", "uiautomator2:chromedriver_autodownload"]
|
|
2746
|
+
: [];
|
|
2747
|
+
// Server start + device boot are environment work: any failure there
|
|
2748
|
+
// (port pressure, an emulator that can't finish booting on this host,
|
|
2749
|
+
// simctl trouble) is a gating SKIP with the reason named — never a
|
|
2750
|
+
// FAIL — matching the mobile rule that every environment gap SKIPs.
|
|
2751
|
+
let acquired;
|
|
2752
|
+
try {
|
|
2753
|
+
const server = await startAppiumServer(appSession.appiumEntry, config, undefined, env, extraArgs);
|
|
2754
|
+
appSession.server = { port: server.port, process: server.process };
|
|
2755
|
+
const desc = normalizeDeviceDescriptor({
|
|
2756
|
+
contextDevice: context.device,
|
|
2757
|
+
platform: mobileTarget,
|
|
2758
|
+
});
|
|
2759
|
+
acquired =
|
|
2760
|
+
mobileTarget === "android"
|
|
2761
|
+
? await acquireDevice({
|
|
2762
|
+
desc,
|
|
2763
|
+
registry: appSession.androidDeviceRegistry,
|
|
2764
|
+
sdkRoot: appSession.androidSdkRoot,
|
|
2765
|
+
deps: appSession.androidDeviceDeps,
|
|
2766
|
+
})
|
|
2767
|
+
: await acquireSimulator({
|
|
2768
|
+
desc,
|
|
2769
|
+
registry: appSession.iosSimulatorRegistry,
|
|
2770
|
+
deps: appSession.iosSimulatorDeps,
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
catch (error) {
|
|
2774
|
+
const errorMessage = `Skipping context on '${mobileTarget}': couldn't prepare the device for the ${mobileWebBrowserName} session (${error?.message ?? error}). Check the emulator/simulator toolchain (\`doc-detective install ${mobileTarget}\`).`;
|
|
2775
|
+
clog("warning", errorMessage);
|
|
2776
|
+
contextReport.result = "SKIPPED";
|
|
2777
|
+
contextReport.resultDescription = errorMessage;
|
|
2778
|
+
return contextReport;
|
|
2779
|
+
}
|
|
2780
|
+
if ("skip" in acquired) {
|
|
2781
|
+
clog("warning", acquired.skip);
|
|
2782
|
+
contextReport.result = "SKIPPED";
|
|
2783
|
+
contextReport.resultDescription = acquired.skip;
|
|
2784
|
+
return contextReport;
|
|
2785
|
+
}
|
|
2786
|
+
// The resolved device joins the context report the way resolved
|
|
2787
|
+
// browser versions do.
|
|
2788
|
+
contextReport.device = {
|
|
2789
|
+
...(typeof contextReport.device === "object"
|
|
2790
|
+
? contextReport.device
|
|
2791
|
+
: {}),
|
|
2792
|
+
platform: mobileTarget,
|
|
2793
|
+
name: acquired.entry.name,
|
|
2794
|
+
};
|
|
2795
|
+
const capabilities = buildMobileBrowserCapabilities({
|
|
2796
|
+
platform: mobileTarget,
|
|
2797
|
+
udid: acquired.entry.udid,
|
|
2798
|
+
// The resolved cache root (not the raw config field, which is
|
|
2799
|
+
// usually unset) — the chromedriver autodownload dir lives here.
|
|
2800
|
+
cacheDir: getCacheDir({ cacheDir: config?.cacheDir }),
|
|
2801
|
+
});
|
|
2802
|
+
try {
|
|
2803
|
+
driver = await driverStart(capabilities, appSession.server.port, 2, {
|
|
2804
|
+
cacheDir: config?.cacheDir,
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
catch (error) {
|
|
2808
|
+
// A capable host that can't open the device browser is an
|
|
2809
|
+
// environment gap (the absent-browser precedent): SKIP with the
|
|
2810
|
+
// likely cause named, never FAIL.
|
|
2811
|
+
const hint = mobileTarget === "android"
|
|
2812
|
+
? " Chrome may not be present on this emulator image — managed Android mobile-web needs a `google_apis` system image (see `doc-detective install android`)."
|
|
2813
|
+
: " Check that the simulator runtime includes Safari and that WebDriverAgent can build (see `doc-detective install ios`).";
|
|
2814
|
+
const errorMessage = `Skipping context on '${mobileTarget}': couldn't start the ${mobileWebBrowserName} session on device '${acquired.entry.name}' (${error?.message ?? error}).${hint}`;
|
|
2815
|
+
clog("warning", errorMessage);
|
|
2816
|
+
contextReport.result = "SKIPPED";
|
|
2817
|
+
contextReport.resultDescription = errorMessage;
|
|
2818
|
+
return contextReport;
|
|
2819
|
+
}
|
|
2820
|
+
// Register the device browser as the context's one browser surface so
|
|
2821
|
+
// surface-targeted steps resolve it by engine name, same as desktop.
|
|
2822
|
+
// No additional sessions: one device, one browser.
|
|
2823
|
+
browserSessions = createSessionRegistry({
|
|
2824
|
+
open: async () => {
|
|
2825
|
+
throw new Error("Additional browser sessions aren't supported on a managed device; the device browser is the context's only browser surface.");
|
|
2826
|
+
},
|
|
2827
|
+
isNameTaken: (name) => !!processRegistry?.has(name),
|
|
2828
|
+
});
|
|
2829
|
+
registerSession(browserSessions, {
|
|
2830
|
+
name: String(mobileWebBrowserName).toLowerCase(),
|
|
2831
|
+
engine: String(mobileWebBrowserName).toLowerCase(),
|
|
2832
|
+
driver,
|
|
2833
|
+
});
|
|
2834
|
+
/* c8 ignore stop */
|
|
2835
|
+
}
|
|
2836
|
+
else if (driverRequired) {
|
|
2663
2837
|
// Check out a server for this context's lifetime — released in the
|
|
2664
2838
|
// finally so the next queued context can reuse it.
|
|
2665
2839
|
appiumPort = await appiumPool.acquire();
|
|
@@ -3564,6 +3738,14 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
3564
3738
|
appSession,
|
|
3565
3739
|
});
|
|
3566
3740
|
}
|
|
3741
|
+
else if (typeof step.swipe !== "undefined") {
|
|
3742
|
+
actionResult = await swipeSurface({
|
|
3743
|
+
config: config,
|
|
3744
|
+
step: step,
|
|
3745
|
+
driver: driver,
|
|
3746
|
+
appSession,
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3567
3749
|
else if (typeof step.type !== "undefined") {
|
|
3568
3750
|
actionResult = await typeKeys({
|
|
3569
3751
|
config: config,
|
|
@@ -3633,7 +3815,10 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
3633
3815
|
// Start one Appium server on a free port and resolve once it answers /status.
|
|
3634
3816
|
// Each concurrent runner gets its own server (own port) so parallel contexts
|
|
3635
3817
|
// never create sessions on the same Appium instance.
|
|
3636
|
-
async function startAppiumServer(appiumEntry, config, display, extraEnv
|
|
3818
|
+
async function startAppiumServer(appiumEntry, config, display, extraEnv,
|
|
3819
|
+
// Extra CLI args for the server, e.g. the scoped `--allow-insecure`
|
|
3820
|
+
// chromedriver-autodownload opt-in for android mobile-web sessions.
|
|
3821
|
+
extraArgs) {
|
|
3637
3822
|
const port = await findFreePort();
|
|
3638
3823
|
log(config, "debug", `Starting Appium on port ${port}`);
|
|
3639
3824
|
// When a virtual display is supplied (Linux Xvfb recording), launch the
|
|
@@ -3648,7 +3833,7 @@ async function startAppiumServer(appiumEntry, config, display, extraEnv) {
|
|
|
3648
3833
|
...(extraEnv ?? {}),
|
|
3649
3834
|
}
|
|
3650
3835
|
: process.env;
|
|
3651
|
-
const proc = spawn(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port)], {
|
|
3836
|
+
const proc = spawn(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port), ...(extraArgs ?? [])], {
|
|
3652
3837
|
windowsHide: true,
|
|
3653
3838
|
cwd: path.join(__dirname, "../.."),
|
|
3654
3839
|
env,
|
|
@@ -3693,20 +3878,11 @@ async function appiumIsReady(port, timeoutMs = 120000) {
|
|
|
3693
3878
|
}
|
|
3694
3879
|
// Start the Appium driver specified in `capabilities`.
|
|
3695
3880
|
async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
3696
|
-
//
|
|
3697
|
-
//
|
|
3698
|
-
//
|
|
3699
|
-
//
|
|
3700
|
-
//
|
|
3701
|
-
// accepts, or Appium's proxy to chromedriver drops the socket ->
|
|
3702
|
-
// ECONNREFUSED / ECONNRESET / "socket hang up" / "could not proxy command".
|
|
3703
|
-
// 2. Several Chromes launching at once briefly starve resources and
|
|
3704
|
-
// ChromeDriver "crashed during startup" / "cannot connect to" /
|
|
3705
|
-
// "DevToolsActivePort" / "session not created". A staggered retry lets
|
|
3706
|
-
// the contention clear; it recovers on the next attempt in practice.
|
|
3707
|
-
// Retry these with linear backoff; any other error is a real session-
|
|
3708
|
-
// creation failure and propagates immediately.
|
|
3709
|
-
const TRANSIENT = /ECONNREFUSED|ECONNRESET|socket hang up|could not proxy command|crashed during startup|cannot connect to|DevToolsActivePort|session not created/i;
|
|
3881
|
+
// Retryable session-creation failures (transient races/contention, plus the
|
|
3882
|
+
// client-side timeout abort for slow-startup native sessions) are enumerated
|
|
3883
|
+
// by isRetryableSessionError in ./utils.js. Retry those with linear backoff;
|
|
3884
|
+
// any other error is a real session-creation failure and propagates
|
|
3885
|
+
// immediately.
|
|
3710
3886
|
const wdio = await loadHeavyDep("webdriverio", { ctx });
|
|
3711
3887
|
// The wdio client aborts the POST /session request after connectionRetryTimeout.
|
|
3712
3888
|
// A cold native session can take far longer to create than the 2-minute
|
|
@@ -3741,7 +3917,7 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
|
3741
3917
|
}
|
|
3742
3918
|
catch (err) {
|
|
3743
3919
|
lastError = err;
|
|
3744
|
-
if (!
|
|
3920
|
+
if (!isRetryableSessionError(String(err && err.message), startupCeiling))
|
|
3745
3921
|
throw err;
|
|
3746
3922
|
if (attempt < maxAttempts) {
|
|
3747
3923
|
await new Promise((r) => setTimeout(r, 500 * attempt));
|