doc-detective 4.19.0 → 4.20.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 +86416 -6069
- package/dist/common/src/types/generated/closeSurface_v3.d.ts +79 -13
- package/dist/common/src/types/generated/closeSurface_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/config_v3.d.ts +24 -0
- package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/context_v3.d.ts +24 -0
- package/dist/common/src/types/generated/context_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/report_v3.d.ts +24 -0
- package/dist/common/src/types/generated/report_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts +48 -0
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/screenshot_v3.d.ts +40 -3
- package/dist/common/src/types/generated/screenshot_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/spec_v3.d.ts +24 -0
- package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/startSurface_v3.d.ts +108 -0
- package/dist/common/src/types/generated/startSurface_v3.d.ts.map +1 -0
- package/dist/common/src/types/generated/startSurface_v3.js +7 -0
- package/dist/common/src/types/generated/startSurface_v3.js.map +1 -0
- package/dist/common/src/types/generated/step_v3.d.ts +532 -107
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/surface_v3.d.ts +35 -2
- package/dist/common/src/types/generated/surface_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/test_v3.d.ts +1837 -851
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/type_v3.d.ts +60 -10
- package/dist/common/src/types/generated/type_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/waitUntil_v3.d.ts +22 -8
- package/dist/common/src/types/generated/waitUntil_v3.d.ts.map +1 -1
- package/dist/core/detectTests.d.ts.map +1 -1
- package/dist/core/detectTests.js +26 -1
- package/dist/core/detectTests.js.map +1 -1
- package/dist/core/expressions.d.ts.map +1 -1
- package/dist/core/expressions.js +19 -1
- package/dist/core/expressions.js.map +1 -1
- package/dist/core/resolveTests.d.ts.map +1 -1
- package/dist/core/resolveTests.js +24 -6
- package/dist/core/resolveTests.js.map +1 -1
- package/dist/core/tests/appSurface.d.ts +106 -0
- package/dist/core/tests/appSurface.d.ts.map +1 -0
- package/dist/core/tests/appSurface.js +521 -0
- package/dist/core/tests/appSurface.js.map +1 -0
- package/dist/core/tests/click.d.ts +2 -1
- package/dist/core/tests/click.d.ts.map +1 -1
- package/dist/core/tests/click.js +2 -1
- package/dist/core/tests/click.js.map +1 -1
- package/dist/core/tests/closeSurface.d.ts +3 -1
- package/dist/core/tests/closeSurface.d.ts.map +1 -1
- package/dist/core/tests/closeSurface.js +43 -5
- package/dist/core/tests/closeSurface.js.map +1 -1
- package/dist/core/tests/findElement.d.ts +2 -1
- package/dist/core/tests/findElement.d.ts.map +1 -1
- package/dist/core/tests/findElement.js +59 -1
- package/dist/core/tests/findElement.js.map +1 -1
- package/dist/core/tests/runCode.d.ts.map +1 -1
- package/dist/core/tests/runCode.js +9 -0
- package/dist/core/tests/runCode.js.map +1 -1
- package/dist/core/tests/saveScreenshot.d.ts +2 -1
- package/dist/core/tests/saveScreenshot.d.ts.map +1 -1
- package/dist/core/tests/saveScreenshot.js +57 -12
- package/dist/core/tests/saveScreenshot.js.map +1 -1
- package/dist/core/tests/startRecording.d.ts +2 -1
- package/dist/core/tests/startRecording.d.ts.map +1 -1
- package/dist/core/tests/startRecording.js +11 -5
- package/dist/core/tests/startRecording.js.map +1 -1
- package/dist/core/tests/typeKeys.d.ts +2 -1
- package/dist/core/tests/typeKeys.d.ts.map +1 -1
- package/dist/core/tests/typeKeys.js +84 -4
- package/dist/core/tests/typeKeys.js.map +1 -1
- package/dist/core/tests.d.ts +9 -2
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +233 -32
- package/dist/core/tests.js.map +1 -1
- package/dist/core/utils.d.ts +14 -1
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +90 -1
- package/dist/core/utils.js.map +1 -1
- package/dist/index.cjs +87807 -6698
- package/dist/runtime/loader.d.ts +8 -0
- package/dist/runtime/loader.d.ts.map +1 -1
- package/dist/runtime/loader.js +10 -0
- package/dist/runtime/loader.js.map +1 -1
- package/package.json +2 -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, } from "./utils.js";
|
|
16
|
+
import { log, replaceEnvs, selectSpecsForRun, findFreePort, runConcurrent, runResourceAware, createResourceRegistry, rollUpResults, createAppiumPool, getRunOutputDir, runArchivesArtifacts, sanitizeFilesystemName, evaluateContextRequirements, } 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";
|
|
@@ -33,6 +33,7 @@ import { httpRequest } from "./tests/httpRequest.js";
|
|
|
33
33
|
import { clickElement } from "./tests/click.js";
|
|
34
34
|
import { runCode } from "./tests/runCode.js";
|
|
35
35
|
import { closeSurface } from "./tests/closeSurface.js";
|
|
36
|
+
import { createAppSessionState, appSurfacePreflight, isAppDriverRequired, stepTargetsAppSurface, startAppSurface, teardownAppSession, } from "./tests/appSurface.js";
|
|
36
37
|
import { runBrowserScript } from "./tests/runBrowserScript.js";
|
|
37
38
|
import { dragAndDropElement } from "./tests/dragAndDrop.js";
|
|
38
39
|
import { createSessionRegistry, registerSession, activeDriver, sweepSessions, } from "./tests/browserSessions.js";
|
|
@@ -49,7 +50,7 @@ import http from "node:http";
|
|
|
49
50
|
import https from "node:https";
|
|
50
51
|
import { fileURLToPath } from "node:url";
|
|
51
52
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
52
|
-
export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, shouldRepairBeforeFallback, isSupportedContext, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, killTree, };
|
|
53
|
+
export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, shouldRepairBeforeFallback, isSupportedContext, contextRequirementsSkipMessage, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, killTree, };
|
|
53
54
|
// exports.appiumStart = appiumStart;
|
|
54
55
|
// exports.appiumIsReady = appiumIsReady;
|
|
55
56
|
// exports.driverStart = driverStart;
|
|
@@ -319,6 +320,48 @@ function isSupportedContext({ context, apps, platform }) {
|
|
|
319
320
|
// Return boolean
|
|
320
321
|
return Boolean(isSupportedApp && isSupportedPlatform);
|
|
321
322
|
}
|
|
323
|
+
// Like isDriverRequired, but only counts driver steps that need a BROWSER: a
|
|
324
|
+
// step whose payload targets an app surface (object form) is driven by the
|
|
325
|
+
// app session instead, and the synthetic autoRecord capture is an ffmpeg
|
|
326
|
+
// screen grab — neither may force a default browser into existence (in a
|
|
327
|
+
// browser test the authored steps already require one, so excluding the
|
|
328
|
+
// synthetic step never changes the outcome there).
|
|
329
|
+
function isBrowserRequired({ test }) {
|
|
330
|
+
if (!Array.isArray(test?.steps))
|
|
331
|
+
return false;
|
|
332
|
+
return test.steps.some((step) => !step?.__autoRecord &&
|
|
333
|
+
driverActions.some((action) => typeof step[action] !== "undefined") &&
|
|
334
|
+
!stepTargetsAppSurface(step));
|
|
335
|
+
}
|
|
336
|
+
// Size the browser Appium server pool: the number of concurrent runner jobs
|
|
337
|
+
// that will actually create a BROWSER session. App-only jobs are excluded —
|
|
338
|
+
// they provision their own per-context Appium server (homed where the native
|
|
339
|
+
// driver resolves), so counting them here would start an idle browser server
|
|
340
|
+
// (and, on Linux, an unused Xvfb display). Using isBrowserRequired keeps this
|
|
341
|
+
// count in lockstep with the per-context acquire predicate. Exported for a
|
|
342
|
+
// focused unit test; the deep pool wiring is exercised end-to-end by CI.
|
|
343
|
+
export function browserJobCount(jobs) {
|
|
344
|
+
if (!Array.isArray(jobs))
|
|
345
|
+
return 0;
|
|
346
|
+
return jobs.filter((job) => isBrowserRequired({ test: job?.context }))
|
|
347
|
+
.length;
|
|
348
|
+
}
|
|
349
|
+
// Evaluate a context's `requires` capability gate. Returns null when the gate
|
|
350
|
+
// is absent or fully met; otherwise a skip message naming every unmet
|
|
351
|
+
// requirement, so the context lands as SKIPPED (the same non-failing outcome
|
|
352
|
+
// as a `platforms` mismatch). `deps` is passed through to
|
|
353
|
+
// evaluateContextRequirements for hermetic tests.
|
|
354
|
+
function contextRequirementsSkipMessage({ context, deps, }) {
|
|
355
|
+
if (context?.requires === undefined || context?.requires === null)
|
|
356
|
+
return null;
|
|
357
|
+
const { met, missing } = evaluateContextRequirements({
|
|
358
|
+
requires: context.requires,
|
|
359
|
+
deps,
|
|
360
|
+
});
|
|
361
|
+
if (met)
|
|
362
|
+
return null;
|
|
363
|
+
return `Skipping context on '${context.platform}': unmet requirements — ${missing.join(", ")}.`;
|
|
364
|
+
}
|
|
322
365
|
function getDefaultBrowser({ runnerDetails }) {
|
|
323
366
|
let browser = {};
|
|
324
367
|
const browserNames = ["firefox", "chrome", "safari"];
|
|
@@ -913,12 +956,14 @@ async function runSpecs({ resolvedTests }) {
|
|
|
913
956
|
else if (concurrency.overlappingCaptures && limit > 1) {
|
|
914
957
|
log(config, "warning", "autoRecord is running ffmpeg recordings concurrently on a shared display, so the captured videos will overlap (each context records the whole screen). For isolated concurrent recordings, run on Linux with Xvfb installed.");
|
|
915
958
|
}
|
|
916
|
-
// Start one Appium server per concurrent runner that will actually
|
|
917
|
-
//
|
|
918
|
-
// distinct port, so parallel contexts never create sessions on the
|
|
919
|
-
// server — that contention crashed ChromeDriver when every context
|
|
920
|
-
// one server.
|
|
921
|
-
|
|
959
|
+
// Start one Appium server per concurrent runner that will actually create a
|
|
960
|
+
// BROWSER session (capped at the number of browser contexts). Each server
|
|
961
|
+
// owns a distinct port, so parallel contexts never create sessions on the
|
|
962
|
+
// same server — that contention crashed ChromeDriver when every context
|
|
963
|
+
// shared one server. App-only contexts run on their own per-context server
|
|
964
|
+
// (see startAppSurface) and are excluded here, so an app-only run starts no
|
|
965
|
+
// browser server. Non-driver runs start none.
|
|
966
|
+
const browserPoolJobCount = browserJobCount(sizingJobs);
|
|
922
967
|
let appiumServers = [];
|
|
923
968
|
let appiumPool;
|
|
924
969
|
// Per-server virtual displays (Linux Xvfb) for concurrent ffmpeg recording,
|
|
@@ -927,7 +972,7 @@ async function runSpecs({ resolvedTests }) {
|
|
|
927
972
|
const xvfbProcesses = [];
|
|
928
973
|
const useXvfbDisplays = concurrency.xvfbContexts.length > 0;
|
|
929
974
|
let portToDisplay;
|
|
930
|
-
if (
|
|
975
|
+
if (browserPoolJobCount > 0) {
|
|
931
976
|
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
932
977
|
// Resolve appium's actual JS entrypoint via `require.resolve` (shim
|
|
933
978
|
// node_modules first, runtime cache second) and invoke it with
|
|
@@ -942,7 +987,7 @@ async function runSpecs({ resolvedTests }) {
|
|
|
942
987
|
if (!appiumEntry) {
|
|
943
988
|
throw new Error("appium is not installed. The runtime pre-flight should have installed it; check DOC_DETECTIVE_CACHE_DIR / config.cacheDir or run `doc-detective install runtime appium`.");
|
|
944
989
|
}
|
|
945
|
-
const serverCount = Math.min(limit,
|
|
990
|
+
const serverCount = Math.min(limit, browserPoolJobCount);
|
|
946
991
|
log(config, "debug", `Starting ${serverCount} Appium server(s).`);
|
|
947
992
|
// Start servers one at a time rather than all at once: concurrent
|
|
948
993
|
// findFreePort() calls share a close-to-rebind window (two could hand out
|
|
@@ -1565,10 +1610,14 @@ function selectWarmUpTargets(jobs, runnerDetails) {
|
|
|
1565
1610
|
// would defeat the warm-up/install de-racing the pre-pass exists for.
|
|
1566
1611
|
if (!context.platform)
|
|
1567
1612
|
context.platform = platform;
|
|
1568
|
-
|
|
1613
|
+
// Size and target the warm-up by isBrowserRequired (not isDriverRequired):
|
|
1614
|
+
// app-only contexts run on their own per-context Appium server, so they
|
|
1615
|
+
// must not pull a browser into the pre-pass or get a default browser
|
|
1616
|
+
// written onto their context. Mirrors the browser pool sizing.
|
|
1617
|
+
if (!context.browser && isBrowserRequired({ test: context })) {
|
|
1569
1618
|
context.browser = getDefaultBrowser({ runnerDetails });
|
|
1570
1619
|
}
|
|
1571
|
-
if (!
|
|
1620
|
+
if (!isBrowserRequired({ test: context }))
|
|
1572
1621
|
continue;
|
|
1573
1622
|
// No resolvable browser — runContext skips these per-context with its own
|
|
1574
1623
|
// message; nothing to warm up.
|
|
@@ -2038,8 +2087,11 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2038
2087
|
...config.integrations.openApi,
|
|
2039
2088
|
];
|
|
2040
2089
|
}
|
|
2041
|
-
// If "browser" isn't defined but is required by the test, set it to the
|
|
2042
|
-
|
|
2090
|
+
// If "browser" isn't defined but is required by the test, set it to the
|
|
2091
|
+
// first available browser in the sequence of Firefox, Chrome, Safari.
|
|
2092
|
+
// App-targeted steps don't count: they run on the app session, so an
|
|
2093
|
+
// app-only test never boots a browser it won't use.
|
|
2094
|
+
if (!context.browser && isBrowserRequired({ test: context })) {
|
|
2043
2095
|
context.browser = getDefaultBrowser({ runnerDetails });
|
|
2044
2096
|
}
|
|
2045
2097
|
// Set context report
|
|
@@ -2051,11 +2103,45 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2051
2103
|
};
|
|
2052
2104
|
// Set meta values (create-if-missing — ids aren't guaranteed unique)
|
|
2053
2105
|
metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId] ??= { steps: {} };
|
|
2106
|
+
// `requires` capability gate: any unmet requirement skips the context with a
|
|
2107
|
+
// message naming what's missing. Evaluated only on the platform the context
|
|
2108
|
+
// targets — a different-platform context keeps its platform skip reason, and
|
|
2109
|
+
// requirements are host facts that would be meaningless to probe elsewhere.
|
|
2110
|
+
if (context.platform === platform) {
|
|
2111
|
+
const requirementsSkip = contextRequirementsSkipMessage({ context });
|
|
2112
|
+
if (requirementsSkip) {
|
|
2113
|
+
clog("warning", requirementsSkip);
|
|
2114
|
+
contextReport.result = "SKIPPED";
|
|
2115
|
+
contextReport.resultDescription = requirementsSkip;
|
|
2116
|
+
return contextReport;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
// App-surface preflight (native app phase A1): when the test provisions or
|
|
2120
|
+
// targets app surfaces, verify the platform supports them and the native
|
|
2121
|
+
// driver is (or can be) installed. Unmet -> SKIPPED with the reason, same
|
|
2122
|
+
// gating semantics as `requires`. On success, the context gets an app
|
|
2123
|
+
// session primed with the resolved Appium entry/home for the app server.
|
|
2124
|
+
// Scoped to the current platform like the `requires` gate above: a context
|
|
2125
|
+
// targeting another platform must skip with the platform-mismatch reason,
|
|
2126
|
+
// not pay (or misreport) a driver-install attempt on this host.
|
|
2127
|
+
let appSession;
|
|
2128
|
+
if (context.platform === platform && isAppDriverRequired({ test: context })) {
|
|
2129
|
+
const preflight = await appSurfacePreflight({ config, platform });
|
|
2130
|
+
if (!preflight.ok) {
|
|
2131
|
+
clog("warning", preflight.reason);
|
|
2132
|
+
contextReport.result = "SKIPPED";
|
|
2133
|
+
contextReport.resultDescription = preflight.reason;
|
|
2134
|
+
return contextReport;
|
|
2135
|
+
}
|
|
2136
|
+
appSession = createAppSessionState();
|
|
2137
|
+
appSession.appiumEntry = preflight.appiumEntry;
|
|
2138
|
+
appSession.appiumHome = preflight.appiumHome;
|
|
2139
|
+
}
|
|
2054
2140
|
// If a driver is required but no browser could be resolved (e.g.
|
|
2055
2141
|
// getDefaultBrowser found nothing installed, or the context supplied a
|
|
2056
2142
|
// browser object with no name), skip with an explicit reason instead of
|
|
2057
2143
|
// letting it fail later as "Failed to start context 'undefined'".
|
|
2058
|
-
if (
|
|
2144
|
+
if (isBrowserRequired({ test: context }) && !context.browser?.name) {
|
|
2059
2145
|
const errorMessage = `Skipping context on '${context.platform}': no supported browser is available in the current environment.`;
|
|
2060
2146
|
clog("warning", errorMessage);
|
|
2061
2147
|
contextReport.result = "SKIPPED";
|
|
@@ -2133,7 +2219,8 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2133
2219
|
// Context-level browserFallback (authored on the runOn entry) overrides the
|
|
2134
2220
|
// config-level policy; config defaults to "auto".
|
|
2135
2221
|
const fallbackPolicy = resolveBrowserFallbackPolicy({ context, config });
|
|
2136
|
-
|
|
2222
|
+
// Browser-required (not app): app-targeted steps run on the app session.
|
|
2223
|
+
const driverRequired = isBrowserRequired({ test: context });
|
|
2137
2224
|
const candidateEngines = platformMatches && driverRequired
|
|
2138
2225
|
? buildFallbackCandidates({
|
|
2139
2226
|
requestedName: requestedBrowserName,
|
|
@@ -2185,8 +2272,9 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2185
2272
|
let fellBackNote = "";
|
|
2186
2273
|
let fellBackPinned = false;
|
|
2187
2274
|
if (driverRequired && !appiumPool) {
|
|
2188
|
-
throw new Error("
|
|
2189
|
-
"
|
|
2275
|
+
throw new Error("Browser driver requested but no Appium server pool was created; " +
|
|
2276
|
+
"the pool sizing (browserJobCount) and this context's isBrowserRequired " +
|
|
2277
|
+
"predicate disagreed; this is a bug.");
|
|
2190
2278
|
}
|
|
2191
2279
|
try {
|
|
2192
2280
|
if (driverRequired) {
|
|
@@ -2605,6 +2693,7 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2605
2693
|
openApiDefinitions: context.openApi || [],
|
|
2606
2694
|
},
|
|
2607
2695
|
processRegistry: processRegistry,
|
|
2696
|
+
appSession: appSession,
|
|
2608
2697
|
});
|
|
2609
2698
|
clog("debug", `RESULT: ${r.status}\n${JSON.stringify(r, null, 2)}`);
|
|
2610
2699
|
r.result = r.status;
|
|
@@ -2740,10 +2829,19 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2740
2829
|
// Stop every recording still active at the end of the context (the
|
|
2741
2830
|
// synthetic autoRecord capture plus any explicit record steps the author
|
|
2742
2831
|
// didn't stop). Each produces an ordered stopRecord step report.
|
|
2743
|
-
// Recordings live per session, so sweep every registered driver
|
|
2832
|
+
// Recordings live per session, so sweep every registered driver — and the
|
|
2833
|
+
// app session's recordingHost, which holds app-only-context recordings.
|
|
2744
2834
|
for (const d of sessionDrivers(browserSessions, driver)) {
|
|
2745
2835
|
await stopAllRecordings({ config, context, driver: d, contextReport });
|
|
2746
2836
|
}
|
|
2837
|
+
if (appSession?.recordingHost.state.recordings.length) {
|
|
2838
|
+
await stopAllRecordings({
|
|
2839
|
+
config,
|
|
2840
|
+
context,
|
|
2841
|
+
driver: appSession.recordingHost,
|
|
2842
|
+
contextReport,
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2747
2845
|
}
|
|
2748
2846
|
finally {
|
|
2749
2847
|
// Safety net: if the context threw before the normal sweep above, recordings
|
|
@@ -2759,6 +2857,14 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2759
2857
|
for (const d of sessionDrivers(browserSessions, driver)) {
|
|
2760
2858
|
await stopAllRecordings({ config, context, driver: d, contextReport });
|
|
2761
2859
|
}
|
|
2860
|
+
if (appSession?.recordingHost.state.recordings.length) {
|
|
2861
|
+
await stopAllRecordings({
|
|
2862
|
+
config,
|
|
2863
|
+
context,
|
|
2864
|
+
driver: appSession.recordingHost,
|
|
2865
|
+
contextReport,
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2762
2868
|
}
|
|
2763
2869
|
catch (error) {
|
|
2764
2870
|
clog("error", `Failed to stop recordings during cleanup: ${error?.message ?? error}`);
|
|
@@ -2780,6 +2886,27 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2780
2886
|
clog("error", `Failed to delete driver session: ${error.message}`);
|
|
2781
2887
|
}
|
|
2782
2888
|
}
|
|
2889
|
+
// Tear down app surfaces: close every remaining app session (the driver
|
|
2890
|
+
// terminates apps it launched) and stop the app Appium server. Tree-kill
|
|
2891
|
+
// in callback form so the server process is actually gone before the
|
|
2892
|
+
// context returns (mirrors the run-level killTree closure in runSpecs).
|
|
2893
|
+
if (appSession) {
|
|
2894
|
+
try {
|
|
2895
|
+
await teardownAppSession(appSession, (pid) => new Promise((resolve) => {
|
|
2896
|
+
if (!pid)
|
|
2897
|
+
return resolve();
|
|
2898
|
+
try {
|
|
2899
|
+
kill(pid, "SIGTERM", () => resolve());
|
|
2900
|
+
}
|
|
2901
|
+
catch {
|
|
2902
|
+
resolve();
|
|
2903
|
+
}
|
|
2904
|
+
}));
|
|
2905
|
+
}
|
|
2906
|
+
catch (error) {
|
|
2907
|
+
clog("error", `Failed to tear down app surfaces: ${error?.message ?? error}`);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2783
2910
|
// Return the Appium server to the pool for the next queued context. Always
|
|
2784
2911
|
// runs (even on the driver-start-failure early return) so a port can't
|
|
2785
2912
|
// leak out of the pool and starve later contexts.
|
|
@@ -2867,7 +2994,7 @@ async function stopAllRecordings({ config, context, driver, contextReport, }) {
|
|
|
2867
2994
|
}
|
|
2868
2995
|
}
|
|
2869
2996
|
// Run a specific step
|
|
2870
|
-
async function runStep({ config = {}, context = {}, step, driver, metaValues = {}, options = {}, processRegistry, }) {
|
|
2997
|
+
async function runStep({ config = {}, context = {}, step, driver, metaValues = {}, options = {}, processRegistry, appSession, }) {
|
|
2871
2998
|
let actionResult;
|
|
2872
2999
|
// Load values from environment variables
|
|
2873
3000
|
step = replaceEnvs(step);
|
|
@@ -2876,6 +3003,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
2876
3003
|
config: config,
|
|
2877
3004
|
step: step,
|
|
2878
3005
|
driver: driver,
|
|
3006
|
+
appSession,
|
|
2879
3007
|
});
|
|
2880
3008
|
}
|
|
2881
3009
|
else if (typeof step.dragAndDrop !== "undefined") {
|
|
@@ -2889,10 +3017,15 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
2889
3017
|
actionResult = await checkLink({ config: config, step: step });
|
|
2890
3018
|
}
|
|
2891
3019
|
else if (typeof step.find !== "undefined") {
|
|
2892
|
-
actionResult = await findElement({ config: config, step: step, driver });
|
|
3020
|
+
actionResult = await findElement({ config: config, step: step, driver, appSession });
|
|
2893
3021
|
}
|
|
2894
3022
|
else if (typeof step.stopRecord !== "undefined") {
|
|
2895
|
-
actionResult = await stopRecording({
|
|
3023
|
+
actionResult = await stopRecording({
|
|
3024
|
+
config: config,
|
|
3025
|
+
step: step,
|
|
3026
|
+
// App-only contexts keep recordings on the app session's host.
|
|
3027
|
+
driver: driver ?? appSession?.recordingHost,
|
|
3028
|
+
});
|
|
2896
3029
|
}
|
|
2897
3030
|
else if (typeof step.goTo !== "undefined") {
|
|
2898
3031
|
actionResult = await goTo({ config: config, step: step, driver: driver });
|
|
@@ -2922,24 +3055,51 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
2922
3055
|
});
|
|
2923
3056
|
}
|
|
2924
3057
|
else if (typeof step.record !== "undefined") {
|
|
3058
|
+
// App-only contexts have no browser driver: recordings live on the app
|
|
3059
|
+
// session's recordingHost (same `.state.recordings` shape) instead.
|
|
3060
|
+
// Threaded into startRecording too, so its already-recording dedupe
|
|
3061
|
+
// checks consult the same store this block pushes into.
|
|
3062
|
+
const recordingHost = driver ?? appSession?.recordingHost;
|
|
2925
3063
|
actionResult = await startRecording({
|
|
2926
3064
|
config: config,
|
|
2927
3065
|
context: context,
|
|
2928
3066
|
step: step,
|
|
2929
3067
|
driver: driver,
|
|
3068
|
+
recordingHost,
|
|
2930
3069
|
});
|
|
2931
3070
|
// Push the started recording onto the per-context stack so several can
|
|
2932
3071
|
// overlap. Carry the step's `id`/`name` so a later stopRecord can target
|
|
2933
3072
|
// it (by name) and end-of-context cleanup can identify the synthetic one.
|
|
2934
|
-
if (actionResult.recording) {
|
|
2935
|
-
|
|
2936
|
-
|
|
3073
|
+
if (actionResult.recording && !recordingHost) {
|
|
3074
|
+
// Defensive: no driver session AND no app session — nowhere to track
|
|
3075
|
+
// the handle, so the end-of-context sweep could never stop it. Kill
|
|
3076
|
+
// the capture now and FAIL loudly rather than leak the process.
|
|
3077
|
+
try {
|
|
3078
|
+
actionResult.recording.process?.kill?.();
|
|
3079
|
+
}
|
|
3080
|
+
catch {
|
|
3081
|
+
// best-effort
|
|
3082
|
+
}
|
|
3083
|
+
delete actionResult.recording;
|
|
3084
|
+
actionResult.status = "FAIL";
|
|
3085
|
+
actionResult.description =
|
|
3086
|
+
"A recording started with no driver session or app session to own it; it was stopped. This context cannot record.";
|
|
3087
|
+
}
|
|
3088
|
+
else if (actionResult.recording) {
|
|
3089
|
+
if (!Array.isArray(recordingHost.state.recordings))
|
|
3090
|
+
recordingHost.state.recordings = [];
|
|
2937
3091
|
const handle = actionResult.recording;
|
|
2938
3092
|
handle.id = handle.id ?? randomUUID();
|
|
2939
3093
|
handle.name = handle.name ?? recordStepName(step.record);
|
|
2940
|
-
if (step.__autoRecord)
|
|
3094
|
+
if (step.__autoRecord) {
|
|
2941
3095
|
handle.synthetic = true;
|
|
2942
|
-
|
|
3096
|
+
// App-only context: no window exists yet to crop to. Mark the handle
|
|
3097
|
+
// so the first app surface to open late-binds its window rect as the
|
|
3098
|
+
// crop (startAppSurface), scoping the capture to the app under test.
|
|
3099
|
+
if (!driver && appSession)
|
|
3100
|
+
handle.pendingAppWindowCrop = true;
|
|
3101
|
+
}
|
|
3102
|
+
recordingHost.state.recordings.push(handle);
|
|
2943
3103
|
}
|
|
2944
3104
|
}
|
|
2945
3105
|
else if (typeof step.runCode !== "undefined") {
|
|
@@ -2956,13 +3116,43 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
2956
3116
|
actionResult = await runShell({ config: config, step: step, driver, processRegistry });
|
|
2957
3117
|
}
|
|
2958
3118
|
else if (typeof step.closeSurface !== "undefined") {
|
|
2959
|
-
actionResult = await closeSurface({ config: config, step: step, driver, processRegistry });
|
|
3119
|
+
actionResult = await closeSurface({ config: config, step: step, driver, processRegistry, appSession });
|
|
3120
|
+
}
|
|
3121
|
+
else if (typeof step.startSurface !== "undefined") {
|
|
3122
|
+
if (!appSession) {
|
|
3123
|
+
actionResult = {
|
|
3124
|
+
status: "FAIL",
|
|
3125
|
+
description: "startSurface ran without an app session; this is a runner bug (runContext must preflight app-driver tests).",
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
else {
|
|
3129
|
+
actionResult = await startAppSurface({
|
|
3130
|
+
config: config,
|
|
3131
|
+
step: step,
|
|
3132
|
+
appSession,
|
|
3133
|
+
platform: context?.platform ?? "",
|
|
3134
|
+
serverDeps: {
|
|
3135
|
+
startServer: async (appiumEntry, appiumHome) => {
|
|
3136
|
+
// APPIUM_HOME points the server at the node_modules that holds
|
|
3137
|
+
// the native driver (shim or runtime cache). The preflight has
|
|
3138
|
+
// already invalidated a stale extensions manifest there, so the
|
|
3139
|
+
// server's startup scan discovers the driver.
|
|
3140
|
+
const server = await startAppiumServer(appiumEntry, config, undefined, {
|
|
3141
|
+
APPIUM_HOME: appiumHome,
|
|
3142
|
+
});
|
|
3143
|
+
return { port: server.port, process: server.process };
|
|
3144
|
+
},
|
|
3145
|
+
startDriver: (capabilities, port) => driverStart(capabilities, port, 2, { cacheDir: config?.cacheDir }),
|
|
3146
|
+
},
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
2960
3149
|
}
|
|
2961
3150
|
else if (typeof step.screenshot !== "undefined") {
|
|
2962
3151
|
actionResult = await saveScreenshot({
|
|
2963
3152
|
config: config,
|
|
2964
3153
|
step: step,
|
|
2965
3154
|
driver: driver,
|
|
3155
|
+
appSession,
|
|
2966
3156
|
});
|
|
2967
3157
|
}
|
|
2968
3158
|
else if (typeof step.type !== "undefined") {
|
|
@@ -2971,6 +3161,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
2971
3161
|
step: step,
|
|
2972
3162
|
driver: driver,
|
|
2973
3163
|
processRegistry,
|
|
3164
|
+
appSession,
|
|
2974
3165
|
});
|
|
2975
3166
|
}
|
|
2976
3167
|
else if (typeof step.wait !== "undefined") {
|
|
@@ -2982,8 +3173,10 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
2982
3173
|
description: `Unknown step action: ${JSON.stringify(step)}`,
|
|
2983
3174
|
};
|
|
2984
3175
|
}
|
|
2985
|
-
// If recording, wait until browser is loaded, then instantiate cursor
|
|
2986
|
-
|
|
3176
|
+
// If recording, wait until browser is loaded, then instantiate cursor.
|
|
3177
|
+
// The `getUrl` guard skips the synthetic-cursor dance when `driver` is the
|
|
3178
|
+
// app session's recordingHost (a bare state holder, not a browser session).
|
|
3179
|
+
if (isRecordingActive(driver) && typeof driver.getUrl === "function") {
|
|
2987
3180
|
const currentUrl = await driver.getUrl();
|
|
2988
3181
|
if (currentUrl !== driver.state.url) {
|
|
2989
3182
|
driver.state.url = currentUrl;
|
|
@@ -3031,13 +3224,21 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
3031
3224
|
// Start one Appium server on a free port and resolve once it answers /status.
|
|
3032
3225
|
// Each concurrent runner gets its own server (own port) so parallel contexts
|
|
3033
3226
|
// never create sessions on the same Appium instance.
|
|
3034
|
-
async function startAppiumServer(appiumEntry, config, display) {
|
|
3227
|
+
async function startAppiumServer(appiumEntry, config, display, extraEnv) {
|
|
3035
3228
|
const port = await findFreePort();
|
|
3036
3229
|
log(config, "debug", `Starting Appium on port ${port}`);
|
|
3037
3230
|
// When a virtual display is supplied (Linux Xvfb recording), launch the
|
|
3038
3231
|
// server with DISPLAY set so the browser it spawns (via chromedriver)
|
|
3039
3232
|
// renders on that display — which is what ffmpeg x11grab then captures.
|
|
3040
|
-
|
|
3233
|
+
// `extraEnv` overrides (e.g. APPIUM_HOME for the app-surface server, which
|
|
3234
|
+
// must be homed where the lazily-installed native driver lives).
|
|
3235
|
+
const env = display || extraEnv
|
|
3236
|
+
? {
|
|
3237
|
+
...process.env,
|
|
3238
|
+
...(display ? { DISPLAY: display } : {}),
|
|
3239
|
+
...(extraEnv ?? {}),
|
|
3240
|
+
}
|
|
3241
|
+
: process.env;
|
|
3041
3242
|
const proc = spawn(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port)], {
|
|
3042
3243
|
windowsHide: true,
|
|
3043
3244
|
cwd: path.join(__dirname, "../.."),
|