doc-detective 4.7.0 → 4.8.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/core/appium.d.ts +2 -1
- package/dist/core/appium.d.ts.map +1 -1
- package/dist/core/appium.js +37 -5
- package/dist/core/appium.js.map +1 -1
- package/dist/core/config.d.ts +8 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +14 -6
- package/dist/core/config.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -10
- package/dist/core/index.js.map +1 -1
- package/dist/core/tests/findElement.js +1 -1
- package/dist/core/tests/findElement.js.map +1 -1
- package/dist/core/tests/saveScreenshot.js +2 -2
- package/dist/core/tests/saveScreenshot.js.map +1 -1
- package/dist/core/tests/startRecording.d.ts.map +1 -1
- package/dist/core/tests/startRecording.js +0 -3
- package/dist/core/tests/startRecording.js.map +1 -1
- package/dist/core/tests/stopRecording.d.ts.map +1 -1
- package/dist/core/tests/stopRecording.js +23 -14
- package/dist/core/tests/stopRecording.js.map +1 -1
- package/dist/core/tests/typeKeys.js +2 -2
- package/dist/core/tests/typeKeys.js.map +1 -1
- package/dist/core/tests.d.ts +60 -4
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +771 -379
- package/dist/core/tests.js.map +1 -1
- package/dist/core/utils.d.ts +9 -1
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +57 -1
- package/dist/core/utils.js.map +1 -1
- package/dist/hints/context.d.ts.map +1 -1
- package/dist/hints/context.js +9 -2
- package/dist/hints/context.js.map +1 -1
- package/dist/hints/hints.d.ts.map +1 -1
- package/dist/hints/hints.js +20 -0
- package/dist/hints/hints.js.map +1 -1
- package/dist/hints/types.d.ts +5 -0
- package/dist/hints/types.d.ts.map +1 -1
- package/dist/index.cjs +591 -317
- package/dist/runtime/browsers.d.ts +14 -0
- package/dist/runtime/browsers.d.ts.map +1 -1
- package/dist/runtime/browsers.js +23 -0
- package/dist/runtime/browsers.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +25 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -130089,6 +130089,44 @@ var init_validate = __esm({
|
|
|
130089
130089
|
});
|
|
130090
130090
|
|
|
130091
130091
|
// dist/core/utils.js
|
|
130092
|
+
function createAppiumPool(ports) {
|
|
130093
|
+
const available = [...ports];
|
|
130094
|
+
const waiters = [];
|
|
130095
|
+
return {
|
|
130096
|
+
acquire() {
|
|
130097
|
+
const port = available.shift();
|
|
130098
|
+
if (port !== void 0)
|
|
130099
|
+
return Promise.resolve(port);
|
|
130100
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
130101
|
+
},
|
|
130102
|
+
release(port) {
|
|
130103
|
+
const next = waiters.shift();
|
|
130104
|
+
if (next)
|
|
130105
|
+
next(port);
|
|
130106
|
+
else
|
|
130107
|
+
available.push(port);
|
|
130108
|
+
}
|
|
130109
|
+
};
|
|
130110
|
+
}
|
|
130111
|
+
async function runConcurrent(items, limit, fn) {
|
|
130112
|
+
let next = 0;
|
|
130113
|
+
const workerCount = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
|
|
130114
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
130115
|
+
while (next < items.length) {
|
|
130116
|
+
await fn(items[next++]);
|
|
130117
|
+
}
|
|
130118
|
+
});
|
|
130119
|
+
await Promise.all(workers);
|
|
130120
|
+
}
|
|
130121
|
+
function rollUpResults(children) {
|
|
130122
|
+
if (children.some((child) => child.result === "FAIL"))
|
|
130123
|
+
return "FAIL";
|
|
130124
|
+
if (children.some((child) => child.result === "WARNING"))
|
|
130125
|
+
return "WARNING";
|
|
130126
|
+
if (children.every((child) => child.result === "SKIPPED"))
|
|
130127
|
+
return "SKIPPED";
|
|
130128
|
+
return "PASS";
|
|
130129
|
+
}
|
|
130092
130130
|
async function findFreePort() {
|
|
130093
130131
|
return new Promise((resolve, reject) => {
|
|
130094
130132
|
const server = import_node_net.default.createServer();
|
|
@@ -131001,12 +131039,27 @@ var init_loader = __esm({
|
|
|
131001
131039
|
});
|
|
131002
131040
|
|
|
131003
131041
|
// dist/core/appium.js
|
|
131042
|
+
function appiumHomeForDriverPath(driverEntry) {
|
|
131043
|
+
const marker = `${import_node_path5.default.sep}node_modules${import_node_path5.default.sep}`;
|
|
131044
|
+
const idx = driverEntry.lastIndexOf(marker);
|
|
131045
|
+
return idx === -1 ? null : driverEntry.slice(0, idx);
|
|
131046
|
+
}
|
|
131004
131047
|
function setAppiumHome(ctx = {}) {
|
|
131005
131048
|
const runtimeAppium = import_node_path5.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }), "node_modules", "appium");
|
|
131006
131049
|
if ((0, import_node_fs5.existsSync)(runtimeAppium)) {
|
|
131007
131050
|
process.env.APPIUM_HOME = import_node_path5.default.join(getRuntimeDir({ cacheDir: ctx.cacheDir }));
|
|
131008
131051
|
return;
|
|
131009
131052
|
}
|
|
131053
|
+
for (const driverName of ["appium-chromium-driver", "appium-geckodriver"]) {
|
|
131054
|
+
const driverEntry = resolveHeavyDepPath(driverName, {
|
|
131055
|
+
cacheDir: ctx.cacheDir
|
|
131056
|
+
});
|
|
131057
|
+
const home = driverEntry ? appiumHomeForDriverPath(driverEntry) : null;
|
|
131058
|
+
if (home) {
|
|
131059
|
+
process.env.APPIUM_HOME = home;
|
|
131060
|
+
return;
|
|
131061
|
+
}
|
|
131062
|
+
}
|
|
131010
131063
|
const corePath = import_node_path5.default.join(__dirname3, "../../node_modules");
|
|
131011
131064
|
const pathArray = corePath.split("node_modules");
|
|
131012
131065
|
let appiumParentPath = pathArray[0];
|
|
@@ -131026,6 +131079,7 @@ var init_appium = __esm({
|
|
|
131026
131079
|
import_node_fs5 = require("node:fs");
|
|
131027
131080
|
import_node_url4 = require("node:url");
|
|
131028
131081
|
init_cacheDir();
|
|
131082
|
+
init_loader();
|
|
131029
131083
|
__dirname3 = import_node_path5.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
|
|
131030
131084
|
}
|
|
131031
131085
|
});
|
|
@@ -131604,9 +131658,10 @@ async function setConfig({ config }) {
|
|
|
131604
131658
|
}
|
|
131605
131659
|
function resolveConcurrentRunners(config) {
|
|
131606
131660
|
if (config.concurrentRunners === true) {
|
|
131607
|
-
return Math.min(import_node_os3.default.cpus().length, 4);
|
|
131661
|
+
return Math.max(1, Math.min(import_node_os3.default.cpus().length, 4));
|
|
131608
131662
|
}
|
|
131609
|
-
|
|
131663
|
+
const runners = Math.floor(Number(config.concurrentRunners));
|
|
131664
|
+
return Number.isFinite(runners) && runners >= 1 ? runners : 1;
|
|
131610
131665
|
}
|
|
131611
131666
|
async function loadDescriptions(config) {
|
|
131612
131667
|
if (config?.integrations?.openApi) {
|
|
@@ -132154,8 +132209,19 @@ var browsers_exports = {};
|
|
|
132154
132209
|
__export(browsers_exports, {
|
|
132155
132210
|
BROWSER_CHANNELS: () => BROWSER_CHANNELS,
|
|
132156
132211
|
ensureBrowserInstalled: () => ensureBrowserInstalled,
|
|
132157
|
-
getInstalledBrowsers: () => getInstalledBrowsers
|
|
132212
|
+
getInstalledBrowsers: () => getInstalledBrowsers,
|
|
132213
|
+
requiredBrowserAssets: () => requiredBrowserAssets
|
|
132158
132214
|
});
|
|
132215
|
+
function requiredBrowserAssets(name) {
|
|
132216
|
+
switch ((name ?? "").toLowerCase()) {
|
|
132217
|
+
case "chrome":
|
|
132218
|
+
return ["chrome", "chromedriver"];
|
|
132219
|
+
case "firefox":
|
|
132220
|
+
return ["firefox", "geckodriver"];
|
|
132221
|
+
default:
|
|
132222
|
+
return [];
|
|
132223
|
+
}
|
|
132224
|
+
}
|
|
132159
132225
|
async function loadPuppeteerBrowsers(deps, ctx) {
|
|
132160
132226
|
if (deps.browsersModule)
|
|
132161
132227
|
return deps.browsersModule;
|
|
@@ -135110,6 +135176,7 @@ init_utils();
|
|
|
135110
135176
|
// dist/core/tests.js
|
|
135111
135177
|
var import_tree_kill = __toESM(require("tree-kill"), 1);
|
|
135112
135178
|
init_loader();
|
|
135179
|
+
init_browsers();
|
|
135113
135180
|
var import_node_os8 = __toESM(require("node:os"), 1);
|
|
135114
135181
|
init_utils();
|
|
135115
135182
|
var import_axios6 = __toESM(require("axios"), 1);
|
|
@@ -135787,7 +135854,7 @@ async function typeKeys({ config, step, driver }) {
|
|
|
135787
135854
|
return result;
|
|
135788
135855
|
}
|
|
135789
135856
|
}
|
|
135790
|
-
if (
|
|
135857
|
+
if (driver?.state?.recording) {
|
|
135791
135858
|
let keys = [];
|
|
135792
135859
|
step.type.keys.forEach((key) => {
|
|
135793
135860
|
if (key.startsWith("$") && key.endsWith("$")) {
|
|
@@ -135817,7 +135884,7 @@ async function typeKeys({ config, step, driver }) {
|
|
|
135817
135884
|
});
|
|
135818
135885
|
}
|
|
135819
135886
|
try {
|
|
135820
|
-
if (
|
|
135887
|
+
if (driver?.state?.recording) {
|
|
135821
135888
|
for (let i = 0; i < step.type.keys.length; i++) {
|
|
135822
135889
|
await driver.keys(step.type.keys[i]);
|
|
135823
135890
|
await new Promise((resolve) => setTimeout(resolve, step.type.inputDelay));
|
|
@@ -135981,7 +136048,7 @@ async function findElement({ config, step, driver, click }) {
|
|
|
135981
136048
|
result.description += " Typed keys.";
|
|
135982
136049
|
}
|
|
135983
136050
|
}
|
|
135984
|
-
if (
|
|
136051
|
+
if (driver?.state?.recording) {
|
|
135985
136052
|
await wait({ config, step: { wait: 2e3 }, driver });
|
|
135986
136053
|
}
|
|
135987
136054
|
return result;
|
|
@@ -136883,13 +136950,13 @@ async function saveScreenshot({ config, step, driver }) {
|
|
|
136883
136950
|
await driver.pause(100);
|
|
136884
136951
|
}
|
|
136885
136952
|
try {
|
|
136886
|
-
if (
|
|
136953
|
+
if (driver?.state?.recording) {
|
|
136887
136954
|
await driver.execute(() => {
|
|
136888
136955
|
document.querySelector("dd-mouse-pointer").style.display = "none";
|
|
136889
136956
|
});
|
|
136890
136957
|
}
|
|
136891
136958
|
await driver.saveScreenshot(filePath);
|
|
136892
|
-
if (
|
|
136959
|
+
if (driver?.state?.recording) {
|
|
136893
136960
|
await driver.execute(() => {
|
|
136894
136961
|
document.querySelector("dd-mouse-pointer").style.display = "block";
|
|
136895
136962
|
});
|
|
@@ -137124,7 +137191,6 @@ async function startRecording({ config, context, step, driver }) {
|
|
|
137124
137191
|
return result;
|
|
137125
137192
|
}
|
|
137126
137193
|
if (context?.browser?.name === "chrome" && context?.browser?.headless === false) {
|
|
137127
|
-
config.recording = {};
|
|
137128
137194
|
const documentTitle = await driver.getTitle();
|
|
137129
137195
|
const originalTab = await driver.getWindowHandle();
|
|
137130
137196
|
await driver.execute(() => document.title = "RECORD_ME");
|
|
@@ -137133,7 +137199,6 @@ async function startRecording({ config, context, step, driver }) {
|
|
|
137133
137199
|
await driver.switchToWindow(recorderTab.handle);
|
|
137134
137200
|
await driver.url("chrome://new-tab-page");
|
|
137135
137201
|
await driver.execute(() => document.title = "RECORDER");
|
|
137136
|
-
config.recording.tab = await driver.getWindowHandle();
|
|
137137
137202
|
const recorderStarted = await driver.executeAsync((baseName2, done) => {
|
|
137138
137203
|
let doneCalled = false;
|
|
137139
137204
|
const safeDone = (value) => {
|
|
@@ -137206,7 +137271,6 @@ async function startRecording({ config, context, step, driver }) {
|
|
|
137206
137271
|
captureAndDownload();
|
|
137207
137272
|
}, baseName);
|
|
137208
137273
|
if (!recorderStarted) {
|
|
137209
|
-
config.recording = null;
|
|
137210
137274
|
result.status = "FAIL";
|
|
137211
137275
|
result.description = "Failed to start recording. getDisplayMedia may have been rejected. On macOS, ensure Chrome has screen recording permission in System Preferences > Privacy & Security > Screen Recording.";
|
|
137212
137276
|
log(config, "error", result.description);
|
|
@@ -137264,14 +137328,15 @@ async function stopRecording({ config, step, driver }) {
|
|
|
137264
137328
|
return result;
|
|
137265
137329
|
}
|
|
137266
137330
|
step = isValidStep.object;
|
|
137267
|
-
|
|
137331
|
+
const recording = driver?.state?.recording;
|
|
137332
|
+
if (!recording) {
|
|
137268
137333
|
result.status = "SKIPPED";
|
|
137269
137334
|
result.description = `Recording isn't started.`;
|
|
137270
137335
|
return result;
|
|
137271
137336
|
}
|
|
137272
137337
|
try {
|
|
137273
|
-
if (
|
|
137274
|
-
await driver.switchToWindow(
|
|
137338
|
+
if (recording.type === "MediaRecorder") {
|
|
137339
|
+
await driver.switchToWindow(recording.tab);
|
|
137275
137340
|
const recorderExists = await driver.execute(() => {
|
|
137276
137341
|
return typeof window.recorder !== "undefined" && window.recorder !== null;
|
|
137277
137342
|
});
|
|
@@ -137280,35 +137345,36 @@ async function stopRecording({ config, step, driver }) {
|
|
|
137280
137345
|
result.description = "Recording was not properly started. The recorder object doesn't exist in the browser context.";
|
|
137281
137346
|
const allHandles2 = await driver.getWindowHandles();
|
|
137282
137347
|
await driver.closeWindow();
|
|
137283
|
-
const remainingHandles2 = allHandles2.filter((h) => h !==
|
|
137348
|
+
const remainingHandles2 = allHandles2.filter((h) => h !== recording.tab);
|
|
137284
137349
|
if (remainingHandles2.length > 0) {
|
|
137285
137350
|
await driver.switchToWindow(remainingHandles2[0]);
|
|
137286
137351
|
}
|
|
137287
|
-
|
|
137352
|
+
driver.state.recording = null;
|
|
137288
137353
|
return result;
|
|
137289
137354
|
}
|
|
137290
137355
|
await driver.execute(() => {
|
|
137291
137356
|
window.recorder.stop();
|
|
137292
137357
|
});
|
|
137293
137358
|
let waitCount = 0;
|
|
137294
|
-
while (!import_node_fs13.default.existsSync(
|
|
137359
|
+
while (!import_node_fs13.default.existsSync(recording.downloadPath) && waitCount < 60) {
|
|
137295
137360
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
137296
137361
|
waitCount++;
|
|
137297
137362
|
}
|
|
137298
|
-
if (!import_node_fs13.default.existsSync(
|
|
137363
|
+
if (!import_node_fs13.default.existsSync(recording.downloadPath)) {
|
|
137299
137364
|
result.status = "FAIL";
|
|
137300
137365
|
result.description = "Recording download timed out.";
|
|
137366
|
+
driver.state.recording = null;
|
|
137301
137367
|
return result;
|
|
137302
137368
|
}
|
|
137303
137369
|
const allHandles = await driver.getWindowHandles();
|
|
137304
137370
|
await driver.closeWindow();
|
|
137305
|
-
const remainingHandles = allHandles.filter((h) => h !==
|
|
137371
|
+
const remainingHandles = allHandles.filter((h) => h !== recording.tab);
|
|
137306
137372
|
if (remainingHandles.length > 0) {
|
|
137307
137373
|
await driver.switchToWindow(remainingHandles[0]);
|
|
137308
137374
|
}
|
|
137309
|
-
const targetPath = `${
|
|
137310
|
-
const downloadPath = `${
|
|
137311
|
-
const endMessage = `Finished processing file: ${
|
|
137375
|
+
const targetPath = `${recording.targetPath}`;
|
|
137376
|
+
const downloadPath = `${recording.downloadPath}`;
|
|
137377
|
+
const endMessage = `Finished processing file: ${recording.targetPath}`;
|
|
137312
137378
|
const ffmpegArgs = ["-y", "-i", downloadPath, "-pix_fmt", "yuv420p"];
|
|
137313
137379
|
if (import_node_path13.default.extname(targetPath) === ".gif") {
|
|
137314
137380
|
ffmpegArgs.push("-vf", "scale=iw:-1:flags=lanczos");
|
|
@@ -137331,12 +137397,13 @@ async function stopRecording({ config, step, driver }) {
|
|
|
137331
137397
|
}
|
|
137332
137398
|
}).on("error", reject);
|
|
137333
137399
|
});
|
|
137334
|
-
|
|
137400
|
+
driver.state.recording = null;
|
|
137335
137401
|
} else {
|
|
137336
137402
|
}
|
|
137337
137403
|
} catch (error) {
|
|
137338
137404
|
result.status = "FAIL";
|
|
137339
137405
|
result.description = `Couldn't stop recording. ${error}`;
|
|
137406
|
+
driver.state.recording = null;
|
|
137340
137407
|
return result;
|
|
137341
137408
|
}
|
|
137342
137409
|
return result;
|
|
@@ -138873,6 +138940,14 @@ var driverActions2 = [
|
|
|
138873
138940
|
"type"
|
|
138874
138941
|
];
|
|
138875
138942
|
var KNOWN_BROWSERS = ["firefox", "chrome", "safari", "webkit"];
|
|
138943
|
+
function combinationKey(context) {
|
|
138944
|
+
const rawName = context?.browser?.name;
|
|
138945
|
+
const name = rawName === "webkit" ? "safari" : rawName || "<none>";
|
|
138946
|
+
return `${context?.platform}::${name}`;
|
|
138947
|
+
}
|
|
138948
|
+
function warmUpDecision(prev) {
|
|
138949
|
+
return prev === "failed" ? "skip" : "attempt";
|
|
138950
|
+
}
|
|
138876
138951
|
function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
138877
138952
|
let capabilities = {};
|
|
138878
138953
|
let args = [];
|
|
@@ -138936,8 +139011,10 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
138936
139011
|
args.push(`--auto-select-desktop-capture-source=RECORD_ME`);
|
|
138937
139012
|
if (options.headless)
|
|
138938
139013
|
args.push("--headless", "--disable-gpu");
|
|
138939
|
-
if (process.platform === "linux")
|
|
139014
|
+
if (process.platform === "linux") {
|
|
138940
139015
|
args.push("--no-sandbox");
|
|
139016
|
+
args.push("--disable-dev-shm-usage");
|
|
139017
|
+
}
|
|
138941
139018
|
capabilities = {
|
|
138942
139019
|
platformName: runnerDetails.environment.platform,
|
|
138943
139020
|
"appium:automationName": "Chromium",
|
|
@@ -138965,22 +139042,9 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
138965
139042
|
}
|
|
138966
139043
|
return capabilities;
|
|
138967
139044
|
}
|
|
138968
|
-
function isAppiumRequired(specs) {
|
|
138969
|
-
let appiumRequired = false;
|
|
138970
|
-
specs.forEach((spec) => {
|
|
138971
|
-
spec.tests.forEach((test) => {
|
|
138972
|
-
test.contexts.forEach((context) => {
|
|
138973
|
-
if (isDriverRequired({ test: context })) {
|
|
138974
|
-
appiumRequired = true;
|
|
138975
|
-
}
|
|
138976
|
-
});
|
|
138977
|
-
});
|
|
138978
|
-
});
|
|
138979
|
-
return appiumRequired;
|
|
138980
|
-
}
|
|
138981
139045
|
function isDriverRequired({ test }) {
|
|
138982
139046
|
let driverRequired = false;
|
|
138983
|
-
test.steps.forEach((step) => {
|
|
139047
|
+
(test.steps || []).forEach((step) => {
|
|
138984
139048
|
driverActions2.forEach((action) => {
|
|
138985
139049
|
if (typeof step[action] !== "undefined")
|
|
138986
139050
|
driverRequired = true;
|
|
@@ -139151,9 +139215,10 @@ async function runSpecs({ resolvedTests }) {
|
|
|
139151
139215
|
allowUnsafeSteps: await allowUnsafeSteps({ config })
|
|
139152
139216
|
};
|
|
139153
139217
|
const platform = runnerDetails.environment.platform;
|
|
139154
|
-
|
|
139218
|
+
let availableApps = runnerDetails.availableApps;
|
|
139155
139219
|
const metaValues = { specs: {} };
|
|
139156
|
-
|
|
139220
|
+
const installAttempts = /* @__PURE__ */ new Map();
|
|
139221
|
+
const warmUpResults = /* @__PURE__ */ new Map();
|
|
139157
139222
|
const report = {
|
|
139158
139223
|
summary: {
|
|
139159
139224
|
specs: {
|
|
@@ -139183,293 +139248,128 @@ async function runSpecs({ resolvedTests }) {
|
|
|
139183
139248
|
},
|
|
139184
139249
|
specs: []
|
|
139185
139250
|
};
|
|
139186
|
-
const
|
|
139187
|
-
let appiumPort;
|
|
139188
|
-
if (appiumRequired) {
|
|
139189
|
-
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
139190
|
-
appiumPort = await findFreePort();
|
|
139191
|
-
log(config, "debug", `Starting Appium on port ${appiumPort}`);
|
|
139192
|
-
const appiumEntry = resolveHeavyDepPath("appium", { cacheDir: config?.cacheDir });
|
|
139193
|
-
if (!appiumEntry) {
|
|
139194
|
-
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`.");
|
|
139195
|
-
}
|
|
139196
|
-
appium = (0, import_node_child_process5.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(appiumPort)], {
|
|
139197
|
-
windowsHide: true,
|
|
139198
|
-
cwd: import_node_path18.default.join(__dirname5, "../..")
|
|
139199
|
-
});
|
|
139200
|
-
appium.on("error", (err) => {
|
|
139201
|
-
log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
|
|
139202
|
-
});
|
|
139203
|
-
appium.stdout.on("data", (data) => {
|
|
139204
|
-
});
|
|
139205
|
-
appium.stderr.on("data", (data) => {
|
|
139206
|
-
});
|
|
139207
|
-
try {
|
|
139208
|
-
await appiumIsReady(appiumPort);
|
|
139209
|
-
} catch (error) {
|
|
139210
|
-
try {
|
|
139211
|
-
if (appium && appium.pid)
|
|
139212
|
-
(0, import_tree_kill.default)(appium.pid);
|
|
139213
|
-
} catch {
|
|
139214
|
-
}
|
|
139215
|
-
throw error;
|
|
139216
|
-
}
|
|
139217
|
-
log(config, "debug", "Appium is ready.");
|
|
139218
|
-
}
|
|
139251
|
+
const limit = resolveConcurrentRunners(config);
|
|
139219
139252
|
log(config, "info", "Running test specs.");
|
|
139253
|
+
const jobs = [];
|
|
139220
139254
|
for (const spec of specs) {
|
|
139221
139255
|
log(config, "debug", `SPEC: ${spec.specId}`);
|
|
139222
|
-
|
|
139256
|
+
metaValues.specs[spec.specId] ??= { tests: {} };
|
|
139257
|
+
const specReport = {
|
|
139223
139258
|
specId: spec.specId,
|
|
139224
139259
|
description: spec.description,
|
|
139225
139260
|
contentPath: spec.contentPath,
|
|
139226
139261
|
tests: []
|
|
139227
139262
|
};
|
|
139228
|
-
|
|
139263
|
+
report.specs.push(specReport);
|
|
139229
139264
|
for (const test of spec.tests) {
|
|
139230
139265
|
log(config, "debug", `TEST: ${test.testId}`);
|
|
139231
|
-
|
|
139266
|
+
metaValues.specs[spec.specId].tests[test.testId] ??= { contexts: {} };
|
|
139267
|
+
const testReport = {
|
|
139232
139268
|
testId: test.testId,
|
|
139233
139269
|
description: test.description,
|
|
139234
139270
|
contentPath: test.contentPath,
|
|
139235
139271
|
detectSteps: test.detectSteps,
|
|
139236
|
-
contexts:
|
|
139272
|
+
contexts: new Array(test.contexts.length)
|
|
139237
139273
|
};
|
|
139238
|
-
|
|
139239
|
-
|
|
139240
|
-
|
|
139241
|
-
|
|
139242
|
-
|
|
139243
|
-
|
|
139244
|
-
|
|
139245
|
-
|
|
139246
|
-
|
|
139247
|
-
|
|
139248
|
-
|
|
139249
|
-
|
|
139274
|
+
specReport.tests.push(testReport);
|
|
139275
|
+
test.contexts.forEach((context, slot) => {
|
|
139276
|
+
context.contextId = context.contextId || (0, import_node_crypto5.randomUUID)();
|
|
139277
|
+
jobs.push({ spec, test, context, contexts: testReport.contexts, slot });
|
|
139278
|
+
});
|
|
139279
|
+
}
|
|
139280
|
+
}
|
|
139281
|
+
if (limit > 1 && jobs.some((job) => job.context.steps?.some((step) => step.record !== void 0))) {
|
|
139282
|
+
log(config, "warning", "Tests include record steps while concurrentRunners is greater than 1. Concurrent recordings can capture the wrong window; set concurrentRunners to 1 for recording runs.");
|
|
139283
|
+
}
|
|
139284
|
+
const driverJobCount = jobs.filter((job) => isDriverRequired({ test: job.context })).length;
|
|
139285
|
+
let appiumServers = [];
|
|
139286
|
+
let appiumPool;
|
|
139287
|
+
if (driverJobCount > 0) {
|
|
139288
|
+
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
139289
|
+
const appiumEntry = resolveHeavyDepPath("appium", {
|
|
139290
|
+
cacheDir: config?.cacheDir
|
|
139291
|
+
});
|
|
139292
|
+
if (!appiumEntry) {
|
|
139293
|
+
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`.");
|
|
139294
|
+
}
|
|
139295
|
+
const serverCount = Math.min(limit, driverJobCount);
|
|
139296
|
+
log(config, "debug", `Starting ${serverCount} Appium server(s).`);
|
|
139297
|
+
try {
|
|
139298
|
+
for (let i = 0; i < serverCount; i++) {
|
|
139299
|
+
appiumServers.push(await startAppiumServer(appiumEntry, config));
|
|
139300
|
+
}
|
|
139301
|
+
} catch (error) {
|
|
139302
|
+
for (const server of appiumServers) {
|
|
139303
|
+
try {
|
|
139304
|
+
(0, import_tree_kill.default)(server.process.pid);
|
|
139305
|
+
} catch {
|
|
139250
139306
|
}
|
|
139251
|
-
|
|
139252
|
-
|
|
139253
|
-
|
|
139254
|
-
|
|
139307
|
+
}
|
|
139308
|
+
throw error;
|
|
139309
|
+
}
|
|
139310
|
+
appiumPool = createAppiumPool(appiumServers.map((s) => s.port));
|
|
139311
|
+
}
|
|
139312
|
+
try {
|
|
139313
|
+
if (limit > 1 && appiumPool) {
|
|
139314
|
+
await warmUpContexts({
|
|
139315
|
+
jobs,
|
|
139316
|
+
config,
|
|
139317
|
+
runnerDetails,
|
|
139318
|
+
appiumPool,
|
|
139319
|
+
installAttempts,
|
|
139320
|
+
warmUpResults
|
|
139321
|
+
});
|
|
139322
|
+
}
|
|
139323
|
+
await runConcurrent(jobs, limit, async (job) => {
|
|
139324
|
+
try {
|
|
139325
|
+
job.contexts[job.slot] = await runContext({
|
|
139326
|
+
config,
|
|
139327
|
+
spec: job.spec,
|
|
139328
|
+
test: job.test,
|
|
139329
|
+
context: job.context,
|
|
139330
|
+
runnerDetails,
|
|
139331
|
+
appiumPool,
|
|
139332
|
+
metaValues,
|
|
139333
|
+
installAttempts,
|
|
139334
|
+
warmUpResults,
|
|
139335
|
+
logPrefix: limit > 1 ? `[${job.test.testId}/${job.context.contextId}]` : ""
|
|
139336
|
+
});
|
|
139337
|
+
} catch (error) {
|
|
139338
|
+
const detail = error?.message ?? String(error);
|
|
139339
|
+
log(config, "error", `Context '${job.context.contextId}' crashed: ${detail}`);
|
|
139340
|
+
job.contexts[job.slot] = {
|
|
139341
|
+
contextId: job.context.contextId,
|
|
139342
|
+
platform: job.context.platform,
|
|
139343
|
+
browser: job.context.browser,
|
|
139344
|
+
result: "FAIL",
|
|
139345
|
+
resultDescription: `Unexpected error: ${detail}`,
|
|
139255
139346
|
steps: []
|
|
139256
139347
|
};
|
|
139257
|
-
|
|
139258
|
-
|
|
139259
|
-
|
|
139260
|
-
|
|
139261
|
-
|
|
139262
|
-
|
|
139263
|
-
result: "SKIPPED",
|
|
139264
|
-
resultDescription: errorMessage
|
|
139265
|
-
};
|
|
139266
|
-
report.summary.contexts.skipped++;
|
|
139267
|
-
testReport.contexts.push(contextReport);
|
|
139268
|
-
continue;
|
|
139269
|
-
}
|
|
139270
|
-
const supportedContext = isSupportedContext({
|
|
139271
|
-
context,
|
|
139272
|
-
apps: availableApps,
|
|
139273
|
-
platform
|
|
139274
|
-
});
|
|
139275
|
-
if (!supportedContext) {
|
|
139276
|
-
log(config, "info", `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`);
|
|
139277
|
-
contextReport = { result: "SKIPPED", ...contextReport };
|
|
139278
|
-
report.summary.contexts.skipped++;
|
|
139279
|
-
testReport.contexts.push(contextReport);
|
|
139280
|
-
continue;
|
|
139281
|
-
}
|
|
139282
|
-
log(config, "debug", `CONTEXT:
|
|
139283
|
-
${JSON.stringify(context, null, 2)}`);
|
|
139284
|
-
let driver;
|
|
139285
|
-
if (!context.steps) {
|
|
139286
|
-
context.steps = [];
|
|
139287
|
-
}
|
|
139288
|
-
const driverRequired = isDriverRequired({ test: context });
|
|
139289
|
-
if (driverRequired) {
|
|
139290
|
-
let caps = getDriverCapabilities({
|
|
139291
|
-
runnerDetails,
|
|
139292
|
-
name: context.browser.name,
|
|
139293
|
-
options: {
|
|
139294
|
-
width: context.browser?.window?.width || 1200,
|
|
139295
|
-
height: context.browser?.window?.height || 800,
|
|
139296
|
-
headless: context.browser?.headless !== false
|
|
139297
|
-
}
|
|
139298
|
-
});
|
|
139299
|
-
log(config, "debug", "CAPABILITIES:");
|
|
139300
|
-
log(config, "debug", caps);
|
|
139301
|
-
if (appiumPort === void 0) {
|
|
139302
|
-
throw new Error("Driver requested but Appium was not started. isAppiumRequired(specs) and isDriverRequired(context) disagreed; this is a bug.");
|
|
139303
|
-
}
|
|
139304
|
-
try {
|
|
139305
|
-
driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
|
|
139306
|
-
} catch (error) {
|
|
139307
|
-
try {
|
|
139308
|
-
log(config, "warning", `Failed to start context '${context.browser?.name}' on '${platform}'. Retrying as headless.`);
|
|
139309
|
-
context.browser.headless = true;
|
|
139310
|
-
caps = getDriverCapabilities({
|
|
139311
|
-
runnerDetails,
|
|
139312
|
-
name: context.browser.name,
|
|
139313
|
-
options: {
|
|
139314
|
-
width: context.browser?.window?.width || 1200,
|
|
139315
|
-
height: context.browser?.window?.height || 800,
|
|
139316
|
-
headless: context.browser?.headless !== false
|
|
139317
|
-
}
|
|
139318
|
-
});
|
|
139319
|
-
driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
|
|
139320
|
-
} catch (error2) {
|
|
139321
|
-
let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
|
|
139322
|
-
if (context.browser?.name === "safari")
|
|
139323
|
-
errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
|
|
139324
|
-
log(config, "error", errorMessage);
|
|
139325
|
-
contextReport = {
|
|
139326
|
-
result: "SKIPPED",
|
|
139327
|
-
resultDescription: errorMessage,
|
|
139328
|
-
...contextReport
|
|
139329
|
-
};
|
|
139330
|
-
report.summary.contexts.skipped++;
|
|
139331
|
-
testReport.contexts.push(contextReport);
|
|
139332
|
-
continue;
|
|
139333
|
-
}
|
|
139334
|
-
}
|
|
139335
|
-
if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
|
|
139336
|
-
await setViewportSize(context, driver);
|
|
139337
|
-
} else if (context.browser?.window?.width || context.browser?.window?.height) {
|
|
139338
|
-
const windowSize = await driver.getWindowSize();
|
|
139339
|
-
await driver.setWindowSize(context.browser?.window?.width || windowSize.width, context.browser?.window?.height || windowSize.height);
|
|
139340
|
-
}
|
|
139341
|
-
}
|
|
139342
|
-
let stepExecutionFailed = false;
|
|
139343
|
-
for (let step of context.steps) {
|
|
139344
|
-
if (!step.stepId)
|
|
139345
|
-
step.stepId = (0, import_node_crypto5.randomUUID)();
|
|
139346
|
-
log(config, "debug", `STEP:
|
|
139347
|
-
${JSON.stringify(step, null, 2)}`);
|
|
139348
|
-
if (step.unsafe && runnerDetails.allowUnsafeSteps === false) {
|
|
139349
|
-
log(config, "warning", `Skipping unsafe step: ${step.description} in test ${test.testId} context ${context.contextId}`);
|
|
139350
|
-
const stepReport2 = {
|
|
139351
|
-
...step,
|
|
139352
|
-
result: "SKIPPED",
|
|
139353
|
-
resultDescription: "Skipped because unsafe steps aren't allowed."
|
|
139354
|
-
};
|
|
139355
|
-
contextReport.steps.push(stepReport2);
|
|
139356
|
-
report.summary.steps.skipped++;
|
|
139357
|
-
continue;
|
|
139358
|
-
}
|
|
139359
|
-
if (stepExecutionFailed) {
|
|
139360
|
-
const stepReport2 = {
|
|
139361
|
-
...step,
|
|
139362
|
-
result: "SKIPPED",
|
|
139363
|
-
resultDescription: "Skipped due to previous failure in context."
|
|
139364
|
-
};
|
|
139365
|
-
contextReport.steps.push(stepReport2);
|
|
139366
|
-
report.summary.steps.skipped++;
|
|
139348
|
+
}
|
|
139349
|
+
});
|
|
139350
|
+
for (const specReport of report.specs) {
|
|
139351
|
+
for (const testReport of specReport.tests) {
|
|
139352
|
+
for (const contextReport of testReport.contexts) {
|
|
139353
|
+
if (!contextReport)
|
|
139367
139354
|
continue;
|
|
139355
|
+
for (const stepReport of contextReport.steps) {
|
|
139356
|
+
report.summary.steps[stepReport.result.toLowerCase()]++;
|
|
139368
139357
|
}
|
|
139369
|
-
|
|
139370
|
-
const stepResult = await runStep({
|
|
139371
|
-
config,
|
|
139372
|
-
context,
|
|
139373
|
-
step,
|
|
139374
|
-
driver,
|
|
139375
|
-
metaValues,
|
|
139376
|
-
options: {
|
|
139377
|
-
openApiDefinitions: context.openApi || []
|
|
139378
|
-
}
|
|
139379
|
-
});
|
|
139380
|
-
log(config, "debug", `RESULT: ${stepResult.status}
|
|
139381
|
-
${JSON.stringify(stepResult, null, 2)}`);
|
|
139382
|
-
stepResult.result = stepResult.status;
|
|
139383
|
-
stepResult.resultDescription = stepResult.description;
|
|
139384
|
-
delete stepResult.status;
|
|
139385
|
-
delete stepResult.description;
|
|
139386
|
-
const stepReport = {
|
|
139387
|
-
...step,
|
|
139388
|
-
...stepResult
|
|
139389
|
-
};
|
|
139390
|
-
contextReport.steps.push(stepReport);
|
|
139391
|
-
report.summary.steps[stepReport.result.toLowerCase()]++;
|
|
139392
|
-
if (stepReport.result === "FAIL") {
|
|
139393
|
-
stepExecutionFailed = true;
|
|
139394
|
-
}
|
|
139395
|
-
}
|
|
139396
|
-
if (config.recording) {
|
|
139397
|
-
const stopRecordStep = {
|
|
139398
|
-
stopRecord: true,
|
|
139399
|
-
description: "Stopping recording",
|
|
139400
|
-
stepId: (0, import_node_crypto5.randomUUID)()
|
|
139401
|
-
};
|
|
139402
|
-
const stepResult = await runStep({
|
|
139403
|
-
config,
|
|
139404
|
-
context,
|
|
139405
|
-
step: stopRecordStep,
|
|
139406
|
-
driver,
|
|
139407
|
-
options: {
|
|
139408
|
-
openApiDefinitions: context.openApi || []
|
|
139409
|
-
}
|
|
139410
|
-
});
|
|
139411
|
-
stepResult.result = stepResult.status;
|
|
139412
|
-
stepResult.resultDescription = stepResult.description;
|
|
139413
|
-
delete stepResult.status;
|
|
139414
|
-
delete stepResult.description;
|
|
139415
|
-
const stepReport = {
|
|
139416
|
-
...stopRecordStep,
|
|
139417
|
-
...stepResult
|
|
139418
|
-
};
|
|
139419
|
-
contextReport.steps.push(stepReport);
|
|
139420
|
-
report.summary.steps[stepReport.result.toLowerCase()]++;
|
|
139421
|
-
}
|
|
139422
|
-
let contextResult;
|
|
139423
|
-
if (contextReport.steps.find((step) => step.result === "FAIL"))
|
|
139424
|
-
contextResult = "FAIL";
|
|
139425
|
-
else if (contextReport.steps.find((step) => step.result === "WARNING"))
|
|
139426
|
-
contextResult = "WARNING";
|
|
139427
|
-
else if (contextReport.steps.length === contextReport.steps.filter((step) => step.result === "SKIPPED").length)
|
|
139428
|
-
contextResult = "SKIPPED";
|
|
139429
|
-
else
|
|
139430
|
-
contextResult = "PASS";
|
|
139431
|
-
contextReport = { result: contextResult, ...contextReport };
|
|
139432
|
-
testReport.contexts.push(contextReport);
|
|
139433
|
-
report.summary.contexts[contextResult.toLowerCase()]++;
|
|
139434
|
-
if (driverRequired) {
|
|
139435
|
-
try {
|
|
139436
|
-
await driver.deleteSession();
|
|
139437
|
-
} catch (error) {
|
|
139438
|
-
log(config, "error", `Failed to delete driver session: ${error.message}`);
|
|
139439
|
-
}
|
|
139358
|
+
report.summary.contexts[contextReport.result.toLowerCase()]++;
|
|
139440
139359
|
}
|
|
139360
|
+
testReport.result = rollUpResults(testReport.contexts.filter(Boolean));
|
|
139361
|
+
report.summary.tests[testReport.result.toLowerCase()]++;
|
|
139441
139362
|
}
|
|
139442
|
-
|
|
139443
|
-
|
|
139444
|
-
testResult = "FAIL";
|
|
139445
|
-
else if (testReport.contexts.find((context) => context.result === "WARNING"))
|
|
139446
|
-
testResult = "WARNING";
|
|
139447
|
-
else if (testReport.contexts.length === testReport.contexts.filter((context) => context.result === "SKIPPED").length)
|
|
139448
|
-
testResult = "SKIPPED";
|
|
139449
|
-
else
|
|
139450
|
-
testResult = "PASS";
|
|
139451
|
-
testReport = { result: testResult, ...testReport };
|
|
139452
|
-
specReport.tests.push(testReport);
|
|
139453
|
-
report.summary.tests[testResult.toLowerCase()]++;
|
|
139363
|
+
specReport.result = rollUpResults(specReport.tests);
|
|
139364
|
+
report.summary.specs[specReport.result.toLowerCase()]++;
|
|
139454
139365
|
}
|
|
139455
|
-
|
|
139456
|
-
|
|
139457
|
-
|
|
139458
|
-
|
|
139459
|
-
|
|
139460
|
-
|
|
139461
|
-
|
|
139462
|
-
else
|
|
139463
|
-
specResult = "PASS";
|
|
139464
|
-
specReport = { result: specResult, ...specReport };
|
|
139465
|
-
report.specs.push(specReport);
|
|
139466
|
-
report.summary.specs[specResult.toLowerCase()]++;
|
|
139467
|
-
}
|
|
139468
|
-
if (appium) {
|
|
139469
|
-
log(config, "debug", "Closing Appium server");
|
|
139470
|
-
try {
|
|
139471
|
-
(0, import_tree_kill.default)(appium.pid);
|
|
139472
|
-
} catch {
|
|
139366
|
+
} finally {
|
|
139367
|
+
for (const server of appiumServers) {
|
|
139368
|
+
log(config, "debug", `Closing Appium server on port ${server.port}`);
|
|
139369
|
+
try {
|
|
139370
|
+
(0, import_tree_kill.default)(server.process.pid);
|
|
139371
|
+
} catch {
|
|
139372
|
+
}
|
|
139473
139373
|
}
|
|
139474
139374
|
}
|
|
139475
139375
|
const herettoConfigs = config?.integrations?.heretto || [];
|
|
@@ -139495,6 +139395,330 @@ ${JSON.stringify(stepResult, null, 2)}`);
|
|
|
139495
139395
|
}
|
|
139496
139396
|
return report;
|
|
139497
139397
|
}
|
|
139398
|
+
function selectWarmUpTargets(jobs, runnerDetails) {
|
|
139399
|
+
const platform = runnerDetails.environment.platform;
|
|
139400
|
+
const seen = /* @__PURE__ */ new Set();
|
|
139401
|
+
const targets = [];
|
|
139402
|
+
for (const job of jobs) {
|
|
139403
|
+
const context = job.context;
|
|
139404
|
+
if (!context.steps)
|
|
139405
|
+
context.steps = [];
|
|
139406
|
+
if (!context.platform)
|
|
139407
|
+
context.platform = platform;
|
|
139408
|
+
if (!context.browser && isDriverRequired({ test: context })) {
|
|
139409
|
+
context.browser = getDefaultBrowser({ runnerDetails });
|
|
139410
|
+
}
|
|
139411
|
+
if (!isDriverRequired({ test: context }))
|
|
139412
|
+
continue;
|
|
139413
|
+
if (!context.browser?.name)
|
|
139414
|
+
continue;
|
|
139415
|
+
const combo = combinationKey(context);
|
|
139416
|
+
if (seen.has(combo))
|
|
139417
|
+
continue;
|
|
139418
|
+
seen.add(combo);
|
|
139419
|
+
targets.push({ context, combo });
|
|
139420
|
+
}
|
|
139421
|
+
return targets;
|
|
139422
|
+
}
|
|
139423
|
+
async function warmUpContexts({ jobs, config, runnerDetails, appiumPool, installAttempts, warmUpResults }) {
|
|
139424
|
+
const platform = runnerDetails.environment.platform;
|
|
139425
|
+
for (const { context } of selectWarmUpTargets(jobs, runnerDetails)) {
|
|
139426
|
+
const combo = combinationKey(context);
|
|
139427
|
+
let supported = isSupportedContext({
|
|
139428
|
+
context,
|
|
139429
|
+
apps: runnerDetails.availableApps,
|
|
139430
|
+
platform
|
|
139431
|
+
});
|
|
139432
|
+
if (!supported && context.platform === platform && Array.isArray(context?.steps) && requiredBrowserAssets(context.browser?.name).length > 0) {
|
|
139433
|
+
const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
|
|
139434
|
+
const outcome = await ensureContextBrowserInstalled({
|
|
139435
|
+
browserName: context.browser?.name,
|
|
139436
|
+
config,
|
|
139437
|
+
installAttempts,
|
|
139438
|
+
deps: {
|
|
139439
|
+
ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
|
|
139440
|
+
log
|
|
139441
|
+
}
|
|
139442
|
+
});
|
|
139443
|
+
if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
|
|
139444
|
+
clearAppCache(config);
|
|
139445
|
+
runnerDetails.availableApps = await getAvailableApps({ config });
|
|
139446
|
+
supported = isSupportedContext({
|
|
139447
|
+
context,
|
|
139448
|
+
apps: runnerDetails.availableApps,
|
|
139449
|
+
platform
|
|
139450
|
+
});
|
|
139451
|
+
}
|
|
139452
|
+
}
|
|
139453
|
+
if (!supported)
|
|
139454
|
+
continue;
|
|
139455
|
+
const port = await appiumPool.acquire();
|
|
139456
|
+
let warmDriver;
|
|
139457
|
+
try {
|
|
139458
|
+
const options = {
|
|
139459
|
+
width: context.browser?.window?.width || 1200,
|
|
139460
|
+
height: context.browser?.window?.height || 800,
|
|
139461
|
+
headless: context.browser?.headless !== false
|
|
139462
|
+
};
|
|
139463
|
+
try {
|
|
139464
|
+
warmDriver = await driverStart(getDriverCapabilities({
|
|
139465
|
+
runnerDetails,
|
|
139466
|
+
name: context.browser.name,
|
|
139467
|
+
options
|
|
139468
|
+
}), port, 4, { cacheDir: config?.cacheDir });
|
|
139469
|
+
} catch {
|
|
139470
|
+
log(config, "warning", `Warm-up for ${combo} failed headed; retrying headless.`);
|
|
139471
|
+
warmDriver = await driverStart(getDriverCapabilities({
|
|
139472
|
+
runnerDetails,
|
|
139473
|
+
name: context.browser.name,
|
|
139474
|
+
options: { ...options, headless: true }
|
|
139475
|
+
}), port, 4, { cacheDir: config?.cacheDir });
|
|
139476
|
+
}
|
|
139477
|
+
warmUpResults.set(combo, "ok");
|
|
139478
|
+
log(config, "debug", `Warm-up succeeded for ${combo}.`);
|
|
139479
|
+
} catch (error) {
|
|
139480
|
+
warmUpResults.set(combo, "failed");
|
|
139481
|
+
log(config, "warning", `Warm-up failed for ${combo}; contexts using it will be skipped: ${error?.message ?? String(error)}`);
|
|
139482
|
+
} finally {
|
|
139483
|
+
if (warmDriver) {
|
|
139484
|
+
try {
|
|
139485
|
+
await warmDriver.deleteSession();
|
|
139486
|
+
} catch {
|
|
139487
|
+
}
|
|
139488
|
+
}
|
|
139489
|
+
appiumPool.release(port);
|
|
139490
|
+
}
|
|
139491
|
+
}
|
|
139492
|
+
}
|
|
139493
|
+
async function runContext({ config, spec, test, context, runnerDetails, appiumPool, metaValues, installAttempts, warmUpResults, logPrefix = "" }) {
|
|
139494
|
+
const platform = runnerDetails.environment.platform;
|
|
139495
|
+
let availableApps = runnerDetails.availableApps;
|
|
139496
|
+
const clog = (level, message) => log(config, level, logPrefix && typeof message === "string" ? `${logPrefix} ${message}` : message);
|
|
139497
|
+
if (!context.steps) {
|
|
139498
|
+
context.steps = [];
|
|
139499
|
+
}
|
|
139500
|
+
if (!context.platform)
|
|
139501
|
+
context.platform = runnerDetails.environment.platform;
|
|
139502
|
+
if (config.integrations?.openApi) {
|
|
139503
|
+
context.openApi = [
|
|
139504
|
+
...context.openApi || [],
|
|
139505
|
+
...config.integrations.openApi
|
|
139506
|
+
];
|
|
139507
|
+
}
|
|
139508
|
+
if (!context.browser && isDriverRequired({ test: context })) {
|
|
139509
|
+
context.browser = getDefaultBrowser({ runnerDetails });
|
|
139510
|
+
}
|
|
139511
|
+
const contextReport = {
|
|
139512
|
+
contextId: context.contextId,
|
|
139513
|
+
platform: context.platform,
|
|
139514
|
+
browser: context.browser,
|
|
139515
|
+
steps: []
|
|
139516
|
+
};
|
|
139517
|
+
metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId] ??= { steps: {} };
|
|
139518
|
+
if (isDriverRequired({ test: context }) && !context.browser?.name) {
|
|
139519
|
+
const errorMessage = `Skipping context on '${context.platform}': no supported browser is available in the current environment.`;
|
|
139520
|
+
clog("warning", errorMessage);
|
|
139521
|
+
contextReport.result = "SKIPPED";
|
|
139522
|
+
contextReport.resultDescription = errorMessage;
|
|
139523
|
+
return contextReport;
|
|
139524
|
+
}
|
|
139525
|
+
let supportedContext = isSupportedContext({
|
|
139526
|
+
context,
|
|
139527
|
+
apps: availableApps,
|
|
139528
|
+
platform
|
|
139529
|
+
});
|
|
139530
|
+
let freshInstallRedetected = false;
|
|
139531
|
+
if (!supportedContext && context.platform === platform && // Mirror isSupportedContext's own guard: isDriverRequired iterates
|
|
139532
|
+
// context.steps, so a malformed context without a steps array would
|
|
139533
|
+
// otherwise crash here instead of skipping cleanly.
|
|
139534
|
+
Array.isArray(context?.steps) && isDriverRequired({ test: context }) && requiredBrowserAssets(context.browser?.name).length > 0) {
|
|
139535
|
+
const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
|
|
139536
|
+
const outcome = await ensureContextBrowserInstalled({
|
|
139537
|
+
browserName: context.browser?.name,
|
|
139538
|
+
config,
|
|
139539
|
+
installAttempts,
|
|
139540
|
+
deps: {
|
|
139541
|
+
ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
|
|
139542
|
+
log
|
|
139543
|
+
}
|
|
139544
|
+
});
|
|
139545
|
+
if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
|
|
139546
|
+
freshInstallRedetected = true;
|
|
139547
|
+
clearAppCache(config);
|
|
139548
|
+
availableApps = await getAvailableApps({ config });
|
|
139549
|
+
runnerDetails.availableApps = availableApps;
|
|
139550
|
+
supportedContext = isSupportedContext({
|
|
139551
|
+
context,
|
|
139552
|
+
apps: availableApps,
|
|
139553
|
+
platform
|
|
139554
|
+
});
|
|
139555
|
+
}
|
|
139556
|
+
}
|
|
139557
|
+
if (!supportedContext) {
|
|
139558
|
+
const errorMessage = freshInstallRedetected ? `Skipping context '${context.browser?.name}' on '${context.platform}': the missing browser dependency was installed but still could not be detected.` : `Skipping context. The current system doesn't support this context: {"platform": "${context.platform}", "apps": ${JSON.stringify(context.apps)}}`;
|
|
139559
|
+
clog(freshInstallRedetected ? "warning" : "info", errorMessage);
|
|
139560
|
+
contextReport.result = "SKIPPED";
|
|
139561
|
+
contextReport.resultDescription = errorMessage;
|
|
139562
|
+
return contextReport;
|
|
139563
|
+
}
|
|
139564
|
+
clog("debug", `CONTEXT:
|
|
139565
|
+
${JSON.stringify(context, null, 2)}`);
|
|
139566
|
+
let driver;
|
|
139567
|
+
let appiumPort;
|
|
139568
|
+
const driverRequired = isDriverRequired({ test: context });
|
|
139569
|
+
if (driverRequired && !appiumPool) {
|
|
139570
|
+
throw new Error("Driver requested but no Appium server pool was created; driverJobCount and isDriverRequired(context) disagreed; this is a bug.");
|
|
139571
|
+
}
|
|
139572
|
+
const combo = combinationKey(context);
|
|
139573
|
+
try {
|
|
139574
|
+
if (driverRequired) {
|
|
139575
|
+
if (warmUpDecision(warmUpResults.get(combo)) === "skip") {
|
|
139576
|
+
const errorMessage = `Skipping context '${context.browser?.name}' on '${context.platform}': this context combination could not start a driver earlier in this run.`;
|
|
139577
|
+
clog("warning", errorMessage);
|
|
139578
|
+
contextReport.result = "SKIPPED";
|
|
139579
|
+
contextReport.resultDescription = errorMessage;
|
|
139580
|
+
return contextReport;
|
|
139581
|
+
}
|
|
139582
|
+
appiumPort = await appiumPool.acquire();
|
|
139583
|
+
let caps = getDriverCapabilities({
|
|
139584
|
+
runnerDetails,
|
|
139585
|
+
name: context.browser.name,
|
|
139586
|
+
options: {
|
|
139587
|
+
width: context.browser?.window?.width || 1200,
|
|
139588
|
+
height: context.browser?.window?.height || 800,
|
|
139589
|
+
headless: context.browser?.headless !== false
|
|
139590
|
+
}
|
|
139591
|
+
});
|
|
139592
|
+
clog("debug", "CAPABILITIES:");
|
|
139593
|
+
clog("debug", caps);
|
|
139594
|
+
try {
|
|
139595
|
+
driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
|
|
139596
|
+
} catch (error) {
|
|
139597
|
+
try {
|
|
139598
|
+
clog("warning", `Failed to start context '${context.browser?.name}' on '${platform}'. Retrying as headless.`);
|
|
139599
|
+
context.browser.headless = true;
|
|
139600
|
+
caps = getDriverCapabilities({
|
|
139601
|
+
runnerDetails,
|
|
139602
|
+
name: context.browser.name,
|
|
139603
|
+
options: {
|
|
139604
|
+
width: context.browser?.window?.width || 1200,
|
|
139605
|
+
height: context.browser?.window?.height || 800,
|
|
139606
|
+
headless: context.browser?.headless !== false
|
|
139607
|
+
}
|
|
139608
|
+
});
|
|
139609
|
+
driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
|
|
139610
|
+
} catch (error2) {
|
|
139611
|
+
let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
|
|
139612
|
+
if (context.browser?.name === "safari" || context.browser?.name === "webkit")
|
|
139613
|
+
errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
|
|
139614
|
+
clog("error", errorMessage);
|
|
139615
|
+
if (!warmUpResults.has(combo))
|
|
139616
|
+
warmUpResults.set(combo, "failed");
|
|
139617
|
+
contextReport.result = "SKIPPED";
|
|
139618
|
+
contextReport.resultDescription = errorMessage;
|
|
139619
|
+
return contextReport;
|
|
139620
|
+
}
|
|
139621
|
+
}
|
|
139622
|
+
if (!warmUpResults.has(combo))
|
|
139623
|
+
warmUpResults.set(combo, "ok");
|
|
139624
|
+
if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
|
|
139625
|
+
await setViewportSize(context, driver);
|
|
139626
|
+
} else if (context.browser?.window?.width || context.browser?.window?.height) {
|
|
139627
|
+
const windowSize = await driver.getWindowSize();
|
|
139628
|
+
await driver.setWindowSize(context.browser?.window?.width || windowSize.width, context.browser?.window?.height || windowSize.height);
|
|
139629
|
+
}
|
|
139630
|
+
}
|
|
139631
|
+
let stepExecutionFailed = false;
|
|
139632
|
+
for (let step of context.steps) {
|
|
139633
|
+
if (!step.stepId)
|
|
139634
|
+
step.stepId = (0, import_node_crypto5.randomUUID)();
|
|
139635
|
+
clog("debug", `STEP:
|
|
139636
|
+
${JSON.stringify(step, null, 2)}`);
|
|
139637
|
+
if (step.unsafe && runnerDetails.allowUnsafeSteps === false) {
|
|
139638
|
+
clog("warning", `Skipping unsafe step: ${step.description} in test ${test.testId} context ${context.contextId}`);
|
|
139639
|
+
const stepReport2 = {
|
|
139640
|
+
...step,
|
|
139641
|
+
result: "SKIPPED",
|
|
139642
|
+
resultDescription: "Skipped because unsafe steps aren't allowed."
|
|
139643
|
+
};
|
|
139644
|
+
contextReport.steps.push(stepReport2);
|
|
139645
|
+
continue;
|
|
139646
|
+
}
|
|
139647
|
+
if (stepExecutionFailed) {
|
|
139648
|
+
const stepReport2 = {
|
|
139649
|
+
...step,
|
|
139650
|
+
result: "SKIPPED",
|
|
139651
|
+
resultDescription: "Skipped due to previous failure in context."
|
|
139652
|
+
};
|
|
139653
|
+
contextReport.steps.push(stepReport2);
|
|
139654
|
+
continue;
|
|
139655
|
+
}
|
|
139656
|
+
metaValues.specs[spec.specId].tests[test.testId].contexts[context.contextId].steps[step.stepId] = {};
|
|
139657
|
+
const stepResult = await runStep({
|
|
139658
|
+
config,
|
|
139659
|
+
context,
|
|
139660
|
+
step,
|
|
139661
|
+
driver,
|
|
139662
|
+
metaValues,
|
|
139663
|
+
options: {
|
|
139664
|
+
openApiDefinitions: context.openApi || []
|
|
139665
|
+
}
|
|
139666
|
+
});
|
|
139667
|
+
clog("debug", `RESULT: ${stepResult.status}
|
|
139668
|
+
${JSON.stringify(stepResult, null, 2)}`);
|
|
139669
|
+
stepResult.result = stepResult.status;
|
|
139670
|
+
stepResult.resultDescription = stepResult.description;
|
|
139671
|
+
delete stepResult.status;
|
|
139672
|
+
delete stepResult.description;
|
|
139673
|
+
const stepReport = {
|
|
139674
|
+
...step,
|
|
139675
|
+
...stepResult
|
|
139676
|
+
};
|
|
139677
|
+
contextReport.steps.push(stepReport);
|
|
139678
|
+
if (stepReport.result === "FAIL") {
|
|
139679
|
+
stepExecutionFailed = true;
|
|
139680
|
+
}
|
|
139681
|
+
}
|
|
139682
|
+
if (driver?.state?.recording) {
|
|
139683
|
+
const stopRecordStep = {
|
|
139684
|
+
stopRecord: true,
|
|
139685
|
+
description: "Stopping recording",
|
|
139686
|
+
stepId: (0, import_node_crypto5.randomUUID)()
|
|
139687
|
+
};
|
|
139688
|
+
const stepResult = await runStep({
|
|
139689
|
+
config,
|
|
139690
|
+
context,
|
|
139691
|
+
step: stopRecordStep,
|
|
139692
|
+
driver,
|
|
139693
|
+
options: {
|
|
139694
|
+
openApiDefinitions: context.openApi || []
|
|
139695
|
+
}
|
|
139696
|
+
});
|
|
139697
|
+
stepResult.result = stepResult.status;
|
|
139698
|
+
stepResult.resultDescription = stepResult.description;
|
|
139699
|
+
delete stepResult.status;
|
|
139700
|
+
delete stepResult.description;
|
|
139701
|
+
const stepReport = {
|
|
139702
|
+
...stopRecordStep,
|
|
139703
|
+
...stepResult
|
|
139704
|
+
};
|
|
139705
|
+
contextReport.steps.push(stepReport);
|
|
139706
|
+
}
|
|
139707
|
+
} finally {
|
|
139708
|
+
if (driver) {
|
|
139709
|
+
try {
|
|
139710
|
+
await driver.deleteSession();
|
|
139711
|
+
} catch (error) {
|
|
139712
|
+
clog("error", `Failed to delete driver session: ${error.message}`);
|
|
139713
|
+
}
|
|
139714
|
+
}
|
|
139715
|
+
if (appiumPort !== void 0 && appiumPool) {
|
|
139716
|
+
appiumPool.release(appiumPort);
|
|
139717
|
+
}
|
|
139718
|
+
}
|
|
139719
|
+
contextReport.result = rollUpResults(contextReport.steps);
|
|
139720
|
+
return contextReport;
|
|
139721
|
+
}
|
|
139498
139722
|
async function runStep({ config = {}, context = {}, step, driver, metaValues = {}, options = {} }) {
|
|
139499
139723
|
let actionResult;
|
|
139500
139724
|
step = replaceEnvs(step);
|
|
@@ -139545,7 +139769,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
139545
139769
|
step,
|
|
139546
139770
|
driver
|
|
139547
139771
|
});
|
|
139548
|
-
|
|
139772
|
+
driver.state.recording = actionResult.recording ?? null;
|
|
139549
139773
|
} else if (typeof step.runCode !== "undefined") {
|
|
139550
139774
|
actionResult = await runCode({ config, step });
|
|
139551
139775
|
} else if (typeof step.runShell !== "undefined") {
|
|
@@ -139570,7 +139794,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
139570
139794
|
description: `Unknown step action: ${JSON.stringify(step)}`
|
|
139571
139795
|
};
|
|
139572
139796
|
}
|
|
139573
|
-
if (
|
|
139797
|
+
if (driver?.state?.recording) {
|
|
139574
139798
|
const currentUrl = await driver.getUrl();
|
|
139575
139799
|
if (currentUrl !== driver.state.url) {
|
|
139576
139800
|
driver.state.url = currentUrl;
|
|
@@ -139592,6 +139816,33 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
139592
139816
|
}
|
|
139593
139817
|
return actionResult;
|
|
139594
139818
|
}
|
|
139819
|
+
async function startAppiumServer(appiumEntry, config) {
|
|
139820
|
+
const port = await findFreePort();
|
|
139821
|
+
log(config, "debug", `Starting Appium on port ${port}`);
|
|
139822
|
+
const proc = (0, import_node_child_process5.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port)], {
|
|
139823
|
+
windowsHide: true,
|
|
139824
|
+
cwd: import_node_path18.default.join(__dirname5, "../..")
|
|
139825
|
+
});
|
|
139826
|
+
proc.on("error", (err) => {
|
|
139827
|
+
log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
|
|
139828
|
+
});
|
|
139829
|
+
proc.stdout.on("data", () => {
|
|
139830
|
+
});
|
|
139831
|
+
proc.stderr.on("data", () => {
|
|
139832
|
+
});
|
|
139833
|
+
try {
|
|
139834
|
+
await appiumIsReady(port);
|
|
139835
|
+
} catch (error) {
|
|
139836
|
+
try {
|
|
139837
|
+
if (proc && proc.pid)
|
|
139838
|
+
(0, import_tree_kill.default)(proc.pid);
|
|
139839
|
+
} catch {
|
|
139840
|
+
}
|
|
139841
|
+
throw error;
|
|
139842
|
+
}
|
|
139843
|
+
log(config, "debug", `Appium is ready on port ${port}.`);
|
|
139844
|
+
return { port, process: proc };
|
|
139845
|
+
}
|
|
139595
139846
|
async function appiumIsReady(port, timeoutMs = 12e4) {
|
|
139596
139847
|
let isReady = false;
|
|
139597
139848
|
const start = Date.now();
|
|
@@ -139610,6 +139861,7 @@ async function appiumIsReady(port, timeoutMs = 12e4) {
|
|
|
139610
139861
|
return isReady;
|
|
139611
139862
|
}
|
|
139612
139863
|
async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
139864
|
+
const TRANSIENT = /ECONNREFUSED|ECONNRESET|socket hang up|could not proxy command|crashed during startup|cannot connect to|DevToolsActivePort|session not created/i;
|
|
139613
139865
|
const wdio = await loadHeavyDep("webdriverio", { ctx });
|
|
139614
139866
|
let lastError;
|
|
139615
139867
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -139626,11 +139878,11 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
|
139626
139878
|
waitforTimeout: 12e4
|
|
139627
139879
|
// 2 minutes
|
|
139628
139880
|
});
|
|
139629
|
-
driver.state = { url: "", x: null, y: null };
|
|
139881
|
+
driver.state = { url: "", x: null, y: null, recording: null };
|
|
139630
139882
|
return driver;
|
|
139631
139883
|
} catch (err) {
|
|
139632
139884
|
lastError = err;
|
|
139633
|
-
if (
|
|
139885
|
+
if (!TRANSIENT.test(String(err && err.message)))
|
|
139634
139886
|
throw err;
|
|
139635
139887
|
if (attempt < maxAttempts) {
|
|
139636
139888
|
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
@@ -139667,6 +139919,31 @@ async function ensureChromeAvailable(config, deps) {
|
|
|
139667
139919
|
}
|
|
139668
139920
|
return availableApps;
|
|
139669
139921
|
}
|
|
139922
|
+
async function ensureContextBrowserInstalled({ browserName, config, installAttempts, deps }) {
|
|
139923
|
+
const key = (browserName ?? "<none>").toLowerCase();
|
|
139924
|
+
const cached = installAttempts.get(key);
|
|
139925
|
+
if (cached)
|
|
139926
|
+
return cached;
|
|
139927
|
+
const assets = requiredBrowserAssets(browserName);
|
|
139928
|
+
if (assets.length === 0) {
|
|
139929
|
+
installAttempts.set(key, "notInstallable");
|
|
139930
|
+
return "notInstallable";
|
|
139931
|
+
}
|
|
139932
|
+
const ctx = { cacheDir: config?.cacheDir };
|
|
139933
|
+
const logger = (msg, level = "info") => deps.log?.(config, level === "warn" ? "warning" : level, msg);
|
|
139934
|
+
try {
|
|
139935
|
+
deps.log?.(config, "info", `Browser '${browserName}' is not available; attempting on-demand install of: ${assets.join(", ")}.`);
|
|
139936
|
+
for (const asset of assets) {
|
|
139937
|
+
await deps.ensureBrowser(asset, { ctx, deps: { logger } });
|
|
139938
|
+
}
|
|
139939
|
+
installAttempts.set(key, "installed");
|
|
139940
|
+
return "installed";
|
|
139941
|
+
} catch (err) {
|
|
139942
|
+
deps.log?.(config, "warning", `On-demand install for '${browserName}' failed: ${err?.message ?? err}`);
|
|
139943
|
+
installAttempts.set(key, "failed");
|
|
139944
|
+
return "failed";
|
|
139945
|
+
}
|
|
139946
|
+
}
|
|
139670
139947
|
async function getRunner(options = {}) {
|
|
139671
139948
|
const environment = getEnvironment();
|
|
139672
139949
|
const config = { ...options.config, environment };
|
|
@@ -139880,22 +140157,19 @@ async function runTests(config, options = {}) {
|
|
|
139880
140157
|
if (needs.browsers.size > 0) {
|
|
139881
140158
|
try {
|
|
139882
140159
|
const { getAvailableApps: getAvailableApps2, clearAppCache: clearAppCache2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
139883
|
-
const { ensureBrowserInstalled: ensureBrowserInstalled2 } = await Promise.resolve().then(() => (init_browsers(), browsers_exports));
|
|
140160
|
+
const { ensureBrowserInstalled: ensureBrowserInstalled2, requiredBrowserAssets: requiredBrowserAssets2 } = await Promise.resolve().then(() => (init_browsers(), browsers_exports));
|
|
139884
140161
|
const available = await getAvailableApps2({ config });
|
|
139885
140162
|
const availableNames = new Set(available.map((a) => a.name));
|
|
139886
140163
|
let installedAnything = false;
|
|
139887
140164
|
for (const browser of needs.browsers) {
|
|
139888
140165
|
if (availableNames.has(browser))
|
|
139889
140166
|
continue;
|
|
139890
|
-
|
|
139891
|
-
|
|
139892
|
-
await ensureBrowserInstalled2(
|
|
139893
|
-
installedAnything = true;
|
|
139894
|
-
} else if (browser === "firefox") {
|
|
139895
|
-
await ensureBrowserInstalled2("firefox", { ctx, deps: { logger: preflightLogger } });
|
|
139896
|
-
await ensureBrowserInstalled2("geckodriver", { ctx, deps: { logger: preflightLogger } });
|
|
139897
|
-
installedAnything = true;
|
|
140167
|
+
const assets = requiredBrowserAssets2(browser);
|
|
140168
|
+
for (const asset of assets) {
|
|
140169
|
+
await ensureBrowserInstalled2(asset, { ctx, deps: { logger: preflightLogger } });
|
|
139898
140170
|
}
|
|
140171
|
+
if (assets.length > 0)
|
|
140172
|
+
installedAnything = true;
|
|
139899
140173
|
}
|
|
139900
140174
|
if (installedAnything)
|
|
139901
140175
|
clearAppCache2(config);
|