doc-detective 4.15.2 → 4.16.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 +280 -0
- package/dist/common/src/types/generated/config_v3.d.ts +16 -0
- package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/context_v3.d.ts +12 -0
- package/dist/common/src/types/generated/context_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/report_v3.d.ts +12 -0
- package/dist/common/src/types/generated/report_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts +28 -0
- package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/spec_v3.d.ts +12 -0
- package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/test_v3.d.ts +16 -0
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/core/config.d.ts +28 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +90 -13
- package/dist/core/config.js.map +1 -1
- package/dist/core/resolveTests.d.ts.map +1 -1
- package/dist/core/resolveTests.js +5 -0
- package/dist/core/resolveTests.js.map +1 -1
- package/dist/core/tests.d.ts +65 -2
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +312 -77
- package/dist/core/tests.js.map +1 -1
- package/dist/hints/hints.js +1 -1
- package/dist/hints/hints.js.map +1 -1
- package/dist/index.cjs +1190 -569
- package/dist/runtime/browsers.d.ts +39 -0
- package/dist/runtime/browsers.d.ts.map +1 -1
- package/dist/runtime/browsers.js +269 -39
- package/dist/runtime/browsers.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +18 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/core/tests.js
CHANGED
|
@@ -48,7 +48,7 @@ import http from "node:http";
|
|
|
48
48
|
import https from "node:https";
|
|
49
49
|
import { fileURLToPath } from "node:url";
|
|
50
50
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
-
export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, getDefaultBrowser, isSupportedContext, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, };
|
|
51
|
+
export { runSpecs, runViaApi, getRunner, ensureChromeAvailable, ensureContextBrowserInstalled, combinationKey, warmUpDecision, selectWarmUpTargets, getDriverCapabilities, getDefaultBrowser, buildFallbackCandidates, driverSkipDiagnostic, resolveBrowserFallbackPolicy, shouldRepairBeforeFallback, isSupportedContext, resolveAutoScreenshot, resolveAutoRecord, buildAutoRecordStep, specIsRouted, };
|
|
52
52
|
// exports.appiumStart = appiumStart;
|
|
53
53
|
// exports.appiumIsReady = appiumIsReady;
|
|
54
54
|
// exports.driverStart = driverStart;
|
|
@@ -262,6 +262,104 @@ function getDefaultBrowser({ runnerDetails }) {
|
|
|
262
262
|
}
|
|
263
263
|
return browser;
|
|
264
264
|
}
|
|
265
|
+
// `webkit` is the runtime alias for Safari (context resolution rewrites it),
|
|
266
|
+
// while getAvailableApps reports the engine as `safari`. Normalize so the two
|
|
267
|
+
// names compare and dedupe as one engine.
|
|
268
|
+
function normalizeBrowserName(name) {
|
|
269
|
+
return name === "webkit" ? "safari" : name ?? "";
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Build the ordered list of browser engines to attempt for a context — the
|
|
273
|
+
* heart of the any-browser → any-available-browser fallback. The requested
|
|
274
|
+
* engine is tried first (when it's actually available); then, when the
|
|
275
|
+
* `browserFallback` policy permits, every *other* available engine follows in
|
|
276
|
+
* a stable preference order. Every returned name maps to an available engine —
|
|
277
|
+
* the requested name is preserved as authored (so `webkit` can be returned even
|
|
278
|
+
* though `availableApps` lists it as `safari`), while fallback names come
|
|
279
|
+
* straight from `availableApps`; `webkit` is normalized to `safari` only for
|
|
280
|
+
* the availability lookup and dedupe. getDriverCapabilities accepts both
|
|
281
|
+
* aliases, so either resolves to real binary/driver paths.
|
|
282
|
+
*
|
|
283
|
+
* Policy:
|
|
284
|
+
* - "auto" → fall back for both auto-selected and explicitly pinned browsers.
|
|
285
|
+
* - "explicit" → fall back only when the browser was auto-selected (not pinned).
|
|
286
|
+
* - "off" → never fall back; only the requested engine (if available).
|
|
287
|
+
*
|
|
288
|
+
* Pure and exported so the precedence is unit-testable without a driver.
|
|
289
|
+
*/
|
|
290
|
+
function buildFallbackCandidates({ requestedName, explicit, policy, availableApps, }) {
|
|
291
|
+
const available = new Set((availableApps || []).map((a) => a.name));
|
|
292
|
+
const requestedNorm = normalizeBrowserName(requestedName);
|
|
293
|
+
const candidates = [];
|
|
294
|
+
// Requested engine first — only if it's actually available (has a working
|
|
295
|
+
// driver). If it isn't, we go straight to fallbacks (or skip).
|
|
296
|
+
if (available.has(requestedNorm))
|
|
297
|
+
candidates.push(requestedName);
|
|
298
|
+
const fallbackAllowed = policy === "auto" || (policy === "explicit" && !explicit);
|
|
299
|
+
if (fallbackAllowed) {
|
|
300
|
+
for (const name of ["firefox", "chrome", "safari"]) {
|
|
301
|
+
if (name === requestedNorm)
|
|
302
|
+
continue;
|
|
303
|
+
if (available.has(name))
|
|
304
|
+
candidates.push(name);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return candidates;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Resolve the effective `browserFallback` policy for a context. A context-level
|
|
311
|
+
* value (authored on the `runOn` entry) overrides the config-level value, which
|
|
312
|
+
* itself defaults to `auto`. Pure and exported so the precedence is testable.
|
|
313
|
+
*/
|
|
314
|
+
function resolveBrowserFallbackPolicy({ context, config, }) {
|
|
315
|
+
return context?.browserFallback || config?.browserFallback || "auto";
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Whether to attempt a driver repair before falling back away from a browser
|
|
319
|
+
* whose session just failed to start. We only repair the *requested* engine
|
|
320
|
+
* (so a fallback substitute that fails isn't itself repaired), only when it has
|
|
321
|
+
* installable driver assets (Safari ships with the OS — nothing to repair), and
|
|
322
|
+
* only once per browser per run (a prior attempt already recorded an outcome in
|
|
323
|
+
* `installAttempts`). This is what keeps a present-but-broken driver from
|
|
324
|
+
* causing an unnecessary fallback when a reinstall would have fixed it. Pure
|
|
325
|
+
* and exported so the decision is unit-testable.
|
|
326
|
+
*/
|
|
327
|
+
function shouldRepairBeforeFallback({ candidateName, requestedName, installAttempts, }) {
|
|
328
|
+
if (normalizeBrowserName(candidateName) !== normalizeBrowserName(requestedName)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (requiredBrowserAssets(candidateName).length === 0)
|
|
332
|
+
return false;
|
|
333
|
+
return !installAttempts.has((candidateName ?? "<none>").toLowerCase());
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The diagnostic message recorded when no engine could start a session. Names
|
|
337
|
+
* the requested engine and whether a cross-engine fallback was even attempted,
|
|
338
|
+
* so a present-but-broken driver reads as actionable rather than a generic
|
|
339
|
+
* "Failed to start context" skip.
|
|
340
|
+
*/
|
|
341
|
+
function driverSkipDiagnostic({ requestedName, platform, platformMatches, attemptedFallback, lastError, }) {
|
|
342
|
+
if (!platformMatches) {
|
|
343
|
+
return `Skipping context on '${platform}': this context targets a different platform.`;
|
|
344
|
+
}
|
|
345
|
+
// Name the driver for the actual requested engine so the hint doesn't always
|
|
346
|
+
// point users at geckodriver when Chrome or Safari is the broken one.
|
|
347
|
+
const driverHint = normalizeBrowserName(requestedName) === "firefox"
|
|
348
|
+
? "geckodriver"
|
|
349
|
+
: normalizeBrowserName(requestedName) === "chrome"
|
|
350
|
+
? "chromedriver"
|
|
351
|
+
: normalizeBrowserName(requestedName) === "safari"
|
|
352
|
+
? "safaridriver"
|
|
353
|
+
: "driver";
|
|
354
|
+
let msg = `Skipping context: could not start a browser session for '${requestedName}' on '${platform}'`;
|
|
355
|
+
msg += attemptedFallback
|
|
356
|
+
? `, and no other available browser could start either.`
|
|
357
|
+
: ` and cross-browser fallback is disabled or unavailable.`;
|
|
358
|
+
if (lastError)
|
|
359
|
+
msg += ` Last error: ${lastError}`;
|
|
360
|
+
msg += ` A present-but-broken driver (for example a partially downloaded ${driverHint}) can cause this; reinstall the driver or its browser.`;
|
|
361
|
+
return msg;
|
|
362
|
+
}
|
|
265
363
|
// Set window size to match target viewport size
|
|
266
364
|
async function setViewportSize(context, driver) {
|
|
267
365
|
if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
|
|
@@ -1469,6 +1567,8 @@ async function warmUpContexts({ jobs, config, runnerDetails, appiumPool, install
|
|
|
1469
1567
|
ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
|
|
1470
1568
|
log,
|
|
1471
1569
|
},
|
|
1570
|
+
// Repair a present-but-broken driver, not just install-if-missing.
|
|
1571
|
+
repair: true,
|
|
1472
1572
|
});
|
|
1473
1573
|
if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
|
|
1474
1574
|
clearAppCache(config);
|
|
@@ -1906,6 +2006,10 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1906
2006
|
// only fires for a genuinely-missing browser (rare) and the app list only
|
|
1907
2007
|
// grows, so a sibling reading a slightly stale snapshot still re-detects.
|
|
1908
2008
|
let freshInstallRedetected = false;
|
|
2009
|
+
// The actual on-demand outcome, so the skip message can distinguish "repaired
|
|
2010
|
+
// but still undetected" from "repair failed" instead of always claiming the
|
|
2011
|
+
// dependency was installed.
|
|
2012
|
+
let freshInstallOutcome;
|
|
1909
2013
|
if (!supportedContext &&
|
|
1910
2014
|
context.platform === platform &&
|
|
1911
2015
|
// Mirror isSupportedContext's own guard: isDriverRequired iterates
|
|
@@ -1925,12 +2029,17 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1925
2029
|
ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
|
|
1926
2030
|
log,
|
|
1927
2031
|
},
|
|
2032
|
+
// The browser is unavailable here — possibly because its driver is
|
|
2033
|
+
// present-but-broken — so repair (force a clean driver reinstall +
|
|
2034
|
+
// re-validation), not just install-if-missing.
|
|
2035
|
+
repair: true,
|
|
1928
2036
|
});
|
|
1929
2037
|
// Re-detect after a real attempt regardless of outcome: a "failed" install
|
|
1930
2038
|
// can still have materialized assets before it threw, so a stale snapshot
|
|
1931
2039
|
// could wrongly skip a now-usable browser.
|
|
1932
2040
|
if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
|
|
1933
2041
|
freshInstallRedetected = true;
|
|
2042
|
+
freshInstallOutcome = outcome;
|
|
1934
2043
|
clearAppCache(config);
|
|
1935
2044
|
availableApps = await getAvailableApps({ config });
|
|
1936
2045
|
runnerDetails.availableApps = availableApps;
|
|
@@ -1941,14 +2050,52 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1941
2050
|
});
|
|
1942
2051
|
}
|
|
1943
2052
|
}
|
|
1944
|
-
//
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2053
|
+
// Resolve which engine(s) this context can actually run on. Platform is an
|
|
2054
|
+
// absolute gate (a context authored for another OS never runs and never
|
|
2055
|
+
// falls back); engine availability is not — a broken or unavailable browser
|
|
2056
|
+
// falls back to any other available engine when the `browserFallback` policy
|
|
2057
|
+
// permits. `supportedContext` (computed above for the requested browser) is
|
|
2058
|
+
// now just one input: even when it's false, a fallback engine may carry the run.
|
|
2059
|
+
const platformMatches = context.platform === platform;
|
|
2060
|
+
const requestedBrowserName = context.browser?.name;
|
|
2061
|
+
const explicitlyRequested = context.browser?.explicit === true;
|
|
2062
|
+
// Context-level browserFallback (authored on the runOn entry) overrides the
|
|
2063
|
+
// config-level policy; config defaults to "auto".
|
|
2064
|
+
const fallbackPolicy = resolveBrowserFallbackPolicy({ context, config });
|
|
2065
|
+
const driverRequired = isDriverRequired({ test: context });
|
|
2066
|
+
const candidateEngines = platformMatches && driverRequired
|
|
2067
|
+
? buildFallbackCandidates({
|
|
2068
|
+
requestedName: requestedBrowserName,
|
|
2069
|
+
explicit: explicitlyRequested,
|
|
2070
|
+
policy: fallbackPolicy,
|
|
2071
|
+
availableApps,
|
|
2072
|
+
})
|
|
2073
|
+
: [];
|
|
2074
|
+
// A driver context with no startable engine is skipped with a diagnostic that
|
|
2075
|
+
// names the requested engine and the partial-download cause.
|
|
2076
|
+
if (driverRequired && candidateEngines.length === 0) {
|
|
1948
2077
|
const errorMessage = freshInstallRedetected
|
|
1949
|
-
?
|
|
1950
|
-
|
|
1951
|
-
|
|
2078
|
+
? freshInstallOutcome === "installed"
|
|
2079
|
+
? `Skipping context '${requestedBrowserName}' on '${context.platform}': the missing browser dependency was installed but still could not be detected.`
|
|
2080
|
+
: `Skipping context '${requestedBrowserName}' on '${context.platform}': the on-demand install/repair of its browser dependency failed.`
|
|
2081
|
+
: driverSkipDiagnostic({
|
|
2082
|
+
requestedName: requestedBrowserName ?? "<none>",
|
|
2083
|
+
// driverSkipDiagnostic treats `platform` as the *current* runner
|
|
2084
|
+
// platform (as in the !startedName path), so pass that — not the
|
|
2085
|
+
// context's target platform — or the mismatch message mislabels it.
|
|
2086
|
+
platform,
|
|
2087
|
+
platformMatches,
|
|
2088
|
+
attemptedFallback: false,
|
|
2089
|
+
});
|
|
2090
|
+
clog(platformMatches ? "warning" : "info", errorMessage);
|
|
2091
|
+
contextReport.result = "SKIPPED";
|
|
2092
|
+
contextReport.resultDescription = errorMessage;
|
|
2093
|
+
return contextReport;
|
|
2094
|
+
}
|
|
2095
|
+
// A non-driver context that targets a different platform has nothing to run.
|
|
2096
|
+
if (!driverRequired && !platformMatches) {
|
|
2097
|
+
const errorMessage = `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`;
|
|
2098
|
+
clog("info", errorMessage);
|
|
1952
2099
|
contextReport.result = "SKIPPED";
|
|
1953
2100
|
contextReport.resultDescription = errorMessage;
|
|
1954
2101
|
return contextReport;
|
|
@@ -1956,26 +2103,17 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1956
2103
|
clog("debug", `CONTEXT:\n${JSON.stringify(context, null, 2)}`);
|
|
1957
2104
|
let driver;
|
|
1958
2105
|
let appiumPort;
|
|
1959
|
-
|
|
2106
|
+
// Layer 5 bookkeeping: set when the context ran on a different engine than
|
|
2107
|
+
// requested, so the final result can be annotated — and downgraded PASS →
|
|
2108
|
+
// WARNING when an explicitly pinned engine was substituted.
|
|
2109
|
+
let fellBackNote = "";
|
|
2110
|
+
let fellBackPinned = false;
|
|
1960
2111
|
if (driverRequired && !appiumPool) {
|
|
1961
2112
|
throw new Error("Driver requested but no Appium server pool was created; " +
|
|
1962
2113
|
"driverJobCount and isDriverRequired(context) disagreed; this is a bug.");
|
|
1963
2114
|
}
|
|
1964
|
-
// Warm-up memoization. The first context of each combination acts as the
|
|
1965
|
-
// warm-up; if that combination already failed to start a driver earlier in
|
|
1966
|
-
// this run, skip it outright instead of paying driverStart's retry/backoff
|
|
1967
|
-
// again. Under concurrency this is a best-effort speedup, not correctness —
|
|
1968
|
-
// same-combo contexts may start before one records a result.
|
|
1969
|
-
const combo = combinationKey(context);
|
|
1970
2115
|
try {
|
|
1971
2116
|
if (driverRequired) {
|
|
1972
|
-
if (warmUpDecision(warmUpResults.get(combo)) === "skip") {
|
|
1973
|
-
const errorMessage = `Skipping context '${context.browser?.name}' on '${context.platform}': this context combination could not start a driver earlier in this run.`;
|
|
1974
|
-
clog("warning", errorMessage);
|
|
1975
|
-
contextReport.result = "SKIPPED";
|
|
1976
|
-
contextReport.resultDescription = errorMessage;
|
|
1977
|
-
return contextReport;
|
|
1978
|
-
}
|
|
1979
2117
|
// Check out a server for this context's lifetime — released in the
|
|
1980
2118
|
// finally so the next queued context can reuse it.
|
|
1981
2119
|
appiumPort = await appiumPool.acquire();
|
|
@@ -1991,70 +2129,149 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1991
2129
|
context.__displaySize = XVFB_SCREEN_SIZE;
|
|
1992
2130
|
}
|
|
1993
2131
|
}
|
|
1994
|
-
// Define driver capabilities
|
|
1995
|
-
// TODO: Support custom apps
|
|
1996
2132
|
// Per-context recording identifiers so concurrent Chrome recordings
|
|
1997
2133
|
// auto-select their own window and download to their own dir.
|
|
1998
2134
|
const recordOptions = {
|
|
1999
2135
|
captureSourceTitle: browserCaptureTitle(context.contextId),
|
|
2000
2136
|
downloadDir: browserDownloadDir(context.contextId),
|
|
2001
2137
|
};
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2138
|
+
// Start a session for one engine, headed first then (on failure) headless.
|
|
2139
|
+
const startDriverForBrowser = async (browserName) => {
|
|
2140
|
+
const wantHeadless = context.browser?.headless !== false;
|
|
2141
|
+
const buildCaps = (headless) => getDriverCapabilities({
|
|
2142
|
+
runnerDetails,
|
|
2143
|
+
name: browserName,
|
|
2144
|
+
options: {
|
|
2145
|
+
width: context.browser?.window?.width || 1200,
|
|
2146
|
+
height: context.browser?.window?.height || 800,
|
|
2147
|
+
headless,
|
|
2148
|
+
...recordOptions,
|
|
2149
|
+
},
|
|
2150
|
+
});
|
|
2151
|
+
const startFailure = () => {
|
|
2152
|
+
let error = `Failed to start context '${browserName}' on '${platform}'.`;
|
|
2153
|
+
if (browserName === "safari" || browserName === "webkit") {
|
|
2154
|
+
error +=
|
|
2155
|
+
" Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
|
|
2156
|
+
}
|
|
2157
|
+
return { ok: false, error };
|
|
2158
|
+
};
|
|
2019
2159
|
try {
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2160
|
+
const d = await driverStart(buildCaps(wantHeadless), appiumPort, 4, {
|
|
2161
|
+
cacheDir: config?.cacheDir,
|
|
2162
|
+
});
|
|
2163
|
+
return { ok: true, driver: d, headless: wantHeadless };
|
|
2164
|
+
}
|
|
2165
|
+
catch {
|
|
2166
|
+
// The headed→headless retry only changes inputs when the first
|
|
2167
|
+
// attempt was headed. If it was already headless, a second identical
|
|
2168
|
+
// attempt would just pay driverStart's backoff again — fail fast.
|
|
2169
|
+
if (wantHeadless)
|
|
2170
|
+
return startFailure();
|
|
2171
|
+
try {
|
|
2172
|
+
clog("warning", `Failed to start context '${browserName}' on '${platform}'. Retrying as headless.`);
|
|
2173
|
+
const d = await driverStart(buildCaps(true), appiumPort, 4, {
|
|
2174
|
+
cacheDir: config?.cacheDir,
|
|
2175
|
+
});
|
|
2176
|
+
return { ok: true, driver: d, headless: true };
|
|
2177
|
+
}
|
|
2178
|
+
catch {
|
|
2179
|
+
return startFailure();
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
// Try the requested engine first, then fall back across every other
|
|
2184
|
+
// available engine (policy permitting). The warm-up memo lets a known-bad
|
|
2185
|
+
// combination be skipped without paying driverStart's backoff again.
|
|
2186
|
+
let startedName;
|
|
2187
|
+
let startedHeadless = false;
|
|
2188
|
+
let lastError = "";
|
|
2189
|
+
for (const candidateName of candidateEngines) {
|
|
2190
|
+
const candidateCombo = combinationKey({
|
|
2191
|
+
platform: context.platform,
|
|
2192
|
+
browser: { name: candidateName },
|
|
2193
|
+
});
|
|
2194
|
+
if (warmUpDecision(warmUpResults.get(candidateCombo)) === "skip") {
|
|
2195
|
+
lastError = `context combination '${candidateName}' on '${platform}' could not start a driver earlier in this run.`;
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
let res = await startDriverForBrowser(candidateName);
|
|
2199
|
+
// The requested engine's session failed — its driver may be present but
|
|
2200
|
+
// broken (e.g. a partial download Layer 2 couldn't pre-validate). Repair
|
|
2201
|
+
// it once and retry before falling back to a different browser, so we
|
|
2202
|
+
// don't substitute engines unnecessarily.
|
|
2203
|
+
if (!res.ok &&
|
|
2204
|
+
shouldRepairBeforeFallback({
|
|
2205
|
+
candidateName,
|
|
2206
|
+
requestedName: requestedBrowserName,
|
|
2207
|
+
installAttempts,
|
|
2208
|
+
})) {
|
|
2209
|
+
const outcome = await ensureContextBrowserInstalled({
|
|
2210
|
+
browserName: candidateName,
|
|
2211
|
+
config,
|
|
2212
|
+
installAttempts,
|
|
2213
|
+
deps: {
|
|
2214
|
+
ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
|
|
2215
|
+
log,
|
|
2031
2216
|
},
|
|
2217
|
+
repair: true,
|
|
2032
2218
|
});
|
|
2033
|
-
|
|
2219
|
+
if (outcome === "installed") {
|
|
2220
|
+
clog("info", `Repaired '${candidateName}' driver after a start failure; retrying before falling back.`);
|
|
2221
|
+
res = await startDriverForBrowser(candidateName);
|
|
2222
|
+
}
|
|
2034
2223
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
if (
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
errorMessage +
|
|
2043
|
-
" Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
|
|
2044
|
-
clog("error", errorMessage);
|
|
2045
|
-
// Record the combination as failed so every later context that shares
|
|
2046
|
-
// it is skipped instantly (see the warm-up check above).
|
|
2047
|
-
if (!warmUpResults.has(combo))
|
|
2048
|
-
warmUpResults.set(combo, "failed");
|
|
2049
|
-
contextReport.result = "SKIPPED";
|
|
2050
|
-
contextReport.resultDescription = errorMessage;
|
|
2051
|
-
return contextReport;
|
|
2224
|
+
if (res.ok) {
|
|
2225
|
+
driver = res.driver;
|
|
2226
|
+
startedName = candidateName;
|
|
2227
|
+
startedHeadless = res.headless;
|
|
2228
|
+
if (!warmUpResults.has(candidateCombo))
|
|
2229
|
+
warmUpResults.set(candidateCombo, "ok");
|
|
2230
|
+
break;
|
|
2052
2231
|
}
|
|
2232
|
+
clog("error", res.error);
|
|
2233
|
+
// Record the combination as failed so every later context that shares
|
|
2234
|
+
// it is skipped instantly (see the warm-up check above).
|
|
2235
|
+
if (!warmUpResults.has(candidateCombo))
|
|
2236
|
+
warmUpResults.set(candidateCombo, "failed");
|
|
2237
|
+
lastError = res.error;
|
|
2238
|
+
}
|
|
2239
|
+
if (!startedName) {
|
|
2240
|
+
const errorMessage = driverSkipDiagnostic({
|
|
2241
|
+
requestedName: requestedBrowserName ?? "<none>",
|
|
2242
|
+
platform,
|
|
2243
|
+
platformMatches: true,
|
|
2244
|
+
attemptedFallback: candidateEngines.length > 1,
|
|
2245
|
+
lastError,
|
|
2246
|
+
});
|
|
2247
|
+
clog("error", errorMessage);
|
|
2248
|
+
contextReport.result = "SKIPPED";
|
|
2249
|
+
contextReport.resultDescription = errorMessage;
|
|
2250
|
+
return contextReport;
|
|
2251
|
+
}
|
|
2252
|
+
// Reflect a headless fallback on the context so downstream logic and the
|
|
2253
|
+
// report agree with what actually launched. Replacing context.browser
|
|
2254
|
+
// makes a new object, so re-point contextReport.browser (set earlier to
|
|
2255
|
+
// the original) at it or the report keeps stale headless metadata.
|
|
2256
|
+
if (startedHeadless) {
|
|
2257
|
+
context.browser = { ...context.browser, headless: true };
|
|
2258
|
+
contextReport.browser = context.browser;
|
|
2259
|
+
}
|
|
2260
|
+
// Cross-engine fallback: re-point the context at the engine that ran so
|
|
2261
|
+
// recording, capabilities, and the report all reflect reality, and stash
|
|
2262
|
+
// the Layer 5 note applied to the context result below.
|
|
2263
|
+
if (normalizeBrowserName(startedName) !==
|
|
2264
|
+
normalizeBrowserName(requestedBrowserName)) {
|
|
2265
|
+
fellBackNote = `${requestedBrowserName} unavailable; ran on ${startedName}.`;
|
|
2266
|
+
fellBackPinned = explicitlyRequested;
|
|
2267
|
+
context.browser = { ...context.browser, name: startedName };
|
|
2268
|
+
contextReport.browser = context.browser;
|
|
2269
|
+
contextReport.fallback = {
|
|
2270
|
+
requested: requestedBrowserName,
|
|
2271
|
+
used: startedName,
|
|
2272
|
+
};
|
|
2273
|
+
clog("warning", fellBackNote);
|
|
2053
2274
|
}
|
|
2054
|
-
// Driver started (first attempt or headless retry) — mark this
|
|
2055
|
-
// combination as known-good for the rest of the run.
|
|
2056
|
-
if (!warmUpResults.has(combo))
|
|
2057
|
-
warmUpResults.set(combo, "ok");
|
|
2058
2275
|
if (context.browser?.viewport?.width ||
|
|
2059
2276
|
context.browser?.viewport?.height) {
|
|
2060
2277
|
// Set driver viewport size
|
|
@@ -2451,6 +2668,18 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
2451
2668
|
}
|
|
2452
2669
|
}
|
|
2453
2670
|
contextReport.result = rollUpResults(contextReport.steps);
|
|
2671
|
+
// Layer 5: a context that ran on a fallback engine is annotated so the
|
|
2672
|
+
// substitution is never silent. When the requested engine was explicitly
|
|
2673
|
+
// pinned and the run otherwise passed, downgrade PASS → WARNING so a degraded
|
|
2674
|
+
// run isn't reported as a clean success. An auto-selected browser keeps PASS.
|
|
2675
|
+
if (fellBackNote) {
|
|
2676
|
+
if (fellBackPinned && contextReport.result === "PASS") {
|
|
2677
|
+
contextReport.result = "WARNING";
|
|
2678
|
+
}
|
|
2679
|
+
contextReport.resultDescription = contextReport.resultDescription
|
|
2680
|
+
? `${fellBackNote} ${contextReport.resultDescription}`
|
|
2681
|
+
: fellBackNote;
|
|
2682
|
+
}
|
|
2454
2683
|
return contextReport;
|
|
2455
2684
|
}
|
|
2456
2685
|
// Stop every recording still active on the driver, pushing an ordered
|
|
@@ -2865,7 +3094,7 @@ async function ensureChromeAvailable(config, deps) {
|
|
|
2865
3094
|
* @returns "installed" when all assets installed, "failed" when an install
|
|
2866
3095
|
* threw, or "notInstallable" for browsers with no installable asset (safari).
|
|
2867
3096
|
*/
|
|
2868
|
-
async function ensureContextBrowserInstalled({ browserName, config, installAttempts, deps, }) {
|
|
3097
|
+
async function ensureContextBrowserInstalled({ browserName, config, installAttempts, deps, repair = false, }) {
|
|
2869
3098
|
const key = (browserName ?? "<none>").toLowerCase();
|
|
2870
3099
|
const cached = installAttempts.get(key);
|
|
2871
3100
|
if (cached)
|
|
@@ -2880,9 +3109,15 @@ async function ensureContextBrowserInstalled({ browserName, config, installAttem
|
|
|
2880
3109
|
// "warn" → "warning" the same way provisionChromeRuntime does.
|
|
2881
3110
|
const logger = (msg, level = "info") => deps.log?.(config, level === "warn" ? "warning" : level, msg);
|
|
2882
3111
|
try {
|
|
2883
|
-
deps.log?.(config, "info", `Browser '${browserName}' is not available; attempting on-demand install of: ${assets.join(", ")}.`);
|
|
3112
|
+
deps.log?.(config, "info", `Browser '${browserName}' is not available; attempting on-demand ${repair ? "repair" : "install"} of: ${assets.join(", ")}.`);
|
|
2884
3113
|
for (const asset of assets) {
|
|
2885
|
-
|
|
3114
|
+
// On repair, force every component (browser binary + driver) so a
|
|
3115
|
+
// present-but-broken one is replaced, not just installed-if-missing.
|
|
3116
|
+
await deps.ensureBrowser(asset, {
|
|
3117
|
+
ctx,
|
|
3118
|
+
deps: { logger },
|
|
3119
|
+
force: !!repair,
|
|
3120
|
+
});
|
|
2886
3121
|
}
|
|
2887
3122
|
installAttempts.set(key, "installed");
|
|
2888
3123
|
return "installed";
|