doc-detective 4.7.0-runtime-dependency-detection.1 → 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/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 +20 -4
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +700 -467
- 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 +536 -355
- 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) {
|
|
@@ -135799,7 +135854,7 @@ async function typeKeys({ config, step, driver }) {
|
|
|
135799
135854
|
return result;
|
|
135800
135855
|
}
|
|
135801
135856
|
}
|
|
135802
|
-
if (
|
|
135857
|
+
if (driver?.state?.recording) {
|
|
135803
135858
|
let keys = [];
|
|
135804
135859
|
step.type.keys.forEach((key) => {
|
|
135805
135860
|
if (key.startsWith("$") && key.endsWith("$")) {
|
|
@@ -135829,7 +135884,7 @@ async function typeKeys({ config, step, driver }) {
|
|
|
135829
135884
|
});
|
|
135830
135885
|
}
|
|
135831
135886
|
try {
|
|
135832
|
-
if (
|
|
135887
|
+
if (driver?.state?.recording) {
|
|
135833
135888
|
for (let i = 0; i < step.type.keys.length; i++) {
|
|
135834
135889
|
await driver.keys(step.type.keys[i]);
|
|
135835
135890
|
await new Promise((resolve) => setTimeout(resolve, step.type.inputDelay));
|
|
@@ -135993,7 +136048,7 @@ async function findElement({ config, step, driver, click }) {
|
|
|
135993
136048
|
result.description += " Typed keys.";
|
|
135994
136049
|
}
|
|
135995
136050
|
}
|
|
135996
|
-
if (
|
|
136051
|
+
if (driver?.state?.recording) {
|
|
135997
136052
|
await wait({ config, step: { wait: 2e3 }, driver });
|
|
135998
136053
|
}
|
|
135999
136054
|
return result;
|
|
@@ -136895,13 +136950,13 @@ async function saveScreenshot({ config, step, driver }) {
|
|
|
136895
136950
|
await driver.pause(100);
|
|
136896
136951
|
}
|
|
136897
136952
|
try {
|
|
136898
|
-
if (
|
|
136953
|
+
if (driver?.state?.recording) {
|
|
136899
136954
|
await driver.execute(() => {
|
|
136900
136955
|
document.querySelector("dd-mouse-pointer").style.display = "none";
|
|
136901
136956
|
});
|
|
136902
136957
|
}
|
|
136903
136958
|
await driver.saveScreenshot(filePath);
|
|
136904
|
-
if (
|
|
136959
|
+
if (driver?.state?.recording) {
|
|
136905
136960
|
await driver.execute(() => {
|
|
136906
136961
|
document.querySelector("dd-mouse-pointer").style.display = "block";
|
|
136907
136962
|
});
|
|
@@ -137136,7 +137191,6 @@ async function startRecording({ config, context, step, driver }) {
|
|
|
137136
137191
|
return result;
|
|
137137
137192
|
}
|
|
137138
137193
|
if (context?.browser?.name === "chrome" && context?.browser?.headless === false) {
|
|
137139
|
-
config.recording = {};
|
|
137140
137194
|
const documentTitle = await driver.getTitle();
|
|
137141
137195
|
const originalTab = await driver.getWindowHandle();
|
|
137142
137196
|
await driver.execute(() => document.title = "RECORD_ME");
|
|
@@ -137145,7 +137199,6 @@ async function startRecording({ config, context, step, driver }) {
|
|
|
137145
137199
|
await driver.switchToWindow(recorderTab.handle);
|
|
137146
137200
|
await driver.url("chrome://new-tab-page");
|
|
137147
137201
|
await driver.execute(() => document.title = "RECORDER");
|
|
137148
|
-
config.recording.tab = await driver.getWindowHandle();
|
|
137149
137202
|
const recorderStarted = await driver.executeAsync((baseName2, done) => {
|
|
137150
137203
|
let doneCalled = false;
|
|
137151
137204
|
const safeDone = (value) => {
|
|
@@ -137218,7 +137271,6 @@ async function startRecording({ config, context, step, driver }) {
|
|
|
137218
137271
|
captureAndDownload();
|
|
137219
137272
|
}, baseName);
|
|
137220
137273
|
if (!recorderStarted) {
|
|
137221
|
-
config.recording = null;
|
|
137222
137274
|
result.status = "FAIL";
|
|
137223
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.";
|
|
137224
137276
|
log(config, "error", result.description);
|
|
@@ -137276,14 +137328,15 @@ async function stopRecording({ config, step, driver }) {
|
|
|
137276
137328
|
return result;
|
|
137277
137329
|
}
|
|
137278
137330
|
step = isValidStep.object;
|
|
137279
|
-
|
|
137331
|
+
const recording = driver?.state?.recording;
|
|
137332
|
+
if (!recording) {
|
|
137280
137333
|
result.status = "SKIPPED";
|
|
137281
137334
|
result.description = `Recording isn't started.`;
|
|
137282
137335
|
return result;
|
|
137283
137336
|
}
|
|
137284
137337
|
try {
|
|
137285
|
-
if (
|
|
137286
|
-
await driver.switchToWindow(
|
|
137338
|
+
if (recording.type === "MediaRecorder") {
|
|
137339
|
+
await driver.switchToWindow(recording.tab);
|
|
137287
137340
|
const recorderExists = await driver.execute(() => {
|
|
137288
137341
|
return typeof window.recorder !== "undefined" && window.recorder !== null;
|
|
137289
137342
|
});
|
|
@@ -137292,35 +137345,36 @@ async function stopRecording({ config, step, driver }) {
|
|
|
137292
137345
|
result.description = "Recording was not properly started. The recorder object doesn't exist in the browser context.";
|
|
137293
137346
|
const allHandles2 = await driver.getWindowHandles();
|
|
137294
137347
|
await driver.closeWindow();
|
|
137295
|
-
const remainingHandles2 = allHandles2.filter((h) => h !==
|
|
137348
|
+
const remainingHandles2 = allHandles2.filter((h) => h !== recording.tab);
|
|
137296
137349
|
if (remainingHandles2.length > 0) {
|
|
137297
137350
|
await driver.switchToWindow(remainingHandles2[0]);
|
|
137298
137351
|
}
|
|
137299
|
-
|
|
137352
|
+
driver.state.recording = null;
|
|
137300
137353
|
return result;
|
|
137301
137354
|
}
|
|
137302
137355
|
await driver.execute(() => {
|
|
137303
137356
|
window.recorder.stop();
|
|
137304
137357
|
});
|
|
137305
137358
|
let waitCount = 0;
|
|
137306
|
-
while (!import_node_fs13.default.existsSync(
|
|
137359
|
+
while (!import_node_fs13.default.existsSync(recording.downloadPath) && waitCount < 60) {
|
|
137307
137360
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
137308
137361
|
waitCount++;
|
|
137309
137362
|
}
|
|
137310
|
-
if (!import_node_fs13.default.existsSync(
|
|
137363
|
+
if (!import_node_fs13.default.existsSync(recording.downloadPath)) {
|
|
137311
137364
|
result.status = "FAIL";
|
|
137312
137365
|
result.description = "Recording download timed out.";
|
|
137366
|
+
driver.state.recording = null;
|
|
137313
137367
|
return result;
|
|
137314
137368
|
}
|
|
137315
137369
|
const allHandles = await driver.getWindowHandles();
|
|
137316
137370
|
await driver.closeWindow();
|
|
137317
|
-
const remainingHandles = allHandles.filter((h) => h !==
|
|
137371
|
+
const remainingHandles = allHandles.filter((h) => h !== recording.tab);
|
|
137318
137372
|
if (remainingHandles.length > 0) {
|
|
137319
137373
|
await driver.switchToWindow(remainingHandles[0]);
|
|
137320
137374
|
}
|
|
137321
|
-
const targetPath = `${
|
|
137322
|
-
const downloadPath = `${
|
|
137323
|
-
const endMessage = `Finished processing file: ${
|
|
137375
|
+
const targetPath = `${recording.targetPath}`;
|
|
137376
|
+
const downloadPath = `${recording.downloadPath}`;
|
|
137377
|
+
const endMessage = `Finished processing file: ${recording.targetPath}`;
|
|
137324
137378
|
const ffmpegArgs = ["-y", "-i", downloadPath, "-pix_fmt", "yuv420p"];
|
|
137325
137379
|
if (import_node_path13.default.extname(targetPath) === ".gif") {
|
|
137326
137380
|
ffmpegArgs.push("-vf", "scale=iw:-1:flags=lanczos");
|
|
@@ -137343,12 +137397,13 @@ async function stopRecording({ config, step, driver }) {
|
|
|
137343
137397
|
}
|
|
137344
137398
|
}).on("error", reject);
|
|
137345
137399
|
});
|
|
137346
|
-
|
|
137400
|
+
driver.state.recording = null;
|
|
137347
137401
|
} else {
|
|
137348
137402
|
}
|
|
137349
137403
|
} catch (error) {
|
|
137350
137404
|
result.status = "FAIL";
|
|
137351
137405
|
result.description = `Couldn't stop recording. ${error}`;
|
|
137406
|
+
driver.state.recording = null;
|
|
137352
137407
|
return result;
|
|
137353
137408
|
}
|
|
137354
137409
|
return result;
|
|
@@ -138956,8 +139011,10 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
138956
139011
|
args.push(`--auto-select-desktop-capture-source=RECORD_ME`);
|
|
138957
139012
|
if (options.headless)
|
|
138958
139013
|
args.push("--headless", "--disable-gpu");
|
|
138959
|
-
if (process.platform === "linux")
|
|
139014
|
+
if (process.platform === "linux") {
|
|
138960
139015
|
args.push("--no-sandbox");
|
|
139016
|
+
args.push("--disable-dev-shm-usage");
|
|
139017
|
+
}
|
|
138961
139018
|
capabilities = {
|
|
138962
139019
|
platformName: runnerDetails.environment.platform,
|
|
138963
139020
|
"appium:automationName": "Chromium",
|
|
@@ -138985,22 +139042,9 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
138985
139042
|
}
|
|
138986
139043
|
return capabilities;
|
|
138987
139044
|
}
|
|
138988
|
-
function isAppiumRequired(specs) {
|
|
138989
|
-
let appiumRequired = false;
|
|
138990
|
-
specs.forEach((spec) => {
|
|
138991
|
-
spec.tests.forEach((test) => {
|
|
138992
|
-
test.contexts.forEach((context) => {
|
|
138993
|
-
if (isDriverRequired({ test: context })) {
|
|
138994
|
-
appiumRequired = true;
|
|
138995
|
-
}
|
|
138996
|
-
});
|
|
138997
|
-
});
|
|
138998
|
-
});
|
|
138999
|
-
return appiumRequired;
|
|
139000
|
-
}
|
|
139001
139045
|
function isDriverRequired({ test }) {
|
|
139002
139046
|
let driverRequired = false;
|
|
139003
|
-
test.steps.forEach((step) => {
|
|
139047
|
+
(test.steps || []).forEach((step) => {
|
|
139004
139048
|
driverActions2.forEach((action) => {
|
|
139005
139049
|
if (typeof step[action] !== "undefined")
|
|
139006
139050
|
driverRequired = true;
|
|
@@ -139175,7 +139219,6 @@ async function runSpecs({ resolvedTests }) {
|
|
|
139175
139219
|
const metaValues = { specs: {} };
|
|
139176
139220
|
const installAttempts = /* @__PURE__ */ new Map();
|
|
139177
139221
|
const warmUpResults = /* @__PURE__ */ new Map();
|
|
139178
|
-
let appium;
|
|
139179
139222
|
const report = {
|
|
139180
139223
|
summary: {
|
|
139181
139224
|
specs: {
|
|
@@ -139205,342 +139248,128 @@ async function runSpecs({ resolvedTests }) {
|
|
|
139205
139248
|
},
|
|
139206
139249
|
specs: []
|
|
139207
139250
|
};
|
|
139208
|
-
const
|
|
139209
|
-
let appiumPort;
|
|
139210
|
-
if (appiumRequired) {
|
|
139211
|
-
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
139212
|
-
appiumPort = await findFreePort();
|
|
139213
|
-
log(config, "debug", `Starting Appium on port ${appiumPort}`);
|
|
139214
|
-
const appiumEntry = resolveHeavyDepPath("appium", { cacheDir: config?.cacheDir });
|
|
139215
|
-
if (!appiumEntry) {
|
|
139216
|
-
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`.");
|
|
139217
|
-
}
|
|
139218
|
-
appium = (0, import_node_child_process5.spawn)(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(appiumPort)], {
|
|
139219
|
-
windowsHide: true,
|
|
139220
|
-
cwd: import_node_path18.default.join(__dirname5, "../..")
|
|
139221
|
-
});
|
|
139222
|
-
appium.on("error", (err) => {
|
|
139223
|
-
log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
|
|
139224
|
-
});
|
|
139225
|
-
appium.stdout.on("data", (data) => {
|
|
139226
|
-
});
|
|
139227
|
-
appium.stderr.on("data", (data) => {
|
|
139228
|
-
});
|
|
139229
|
-
try {
|
|
139230
|
-
await appiumIsReady(appiumPort);
|
|
139231
|
-
} catch (error) {
|
|
139232
|
-
try {
|
|
139233
|
-
if (appium && appium.pid)
|
|
139234
|
-
(0, import_tree_kill.default)(appium.pid);
|
|
139235
|
-
} catch {
|
|
139236
|
-
}
|
|
139237
|
-
throw error;
|
|
139238
|
-
}
|
|
139239
|
-
log(config, "debug", "Appium is ready.");
|
|
139240
|
-
}
|
|
139251
|
+
const limit = resolveConcurrentRunners(config);
|
|
139241
139252
|
log(config, "info", "Running test specs.");
|
|
139253
|
+
const jobs = [];
|
|
139242
139254
|
for (const spec of specs) {
|
|
139243
139255
|
log(config, "debug", `SPEC: ${spec.specId}`);
|
|
139244
|
-
|
|
139256
|
+
metaValues.specs[spec.specId] ??= { tests: {} };
|
|
139257
|
+
const specReport = {
|
|
139245
139258
|
specId: spec.specId,
|
|
139246
139259
|
description: spec.description,
|
|
139247
139260
|
contentPath: spec.contentPath,
|
|
139248
139261
|
tests: []
|
|
139249
139262
|
};
|
|
139250
|
-
|
|
139263
|
+
report.specs.push(specReport);
|
|
139251
139264
|
for (const test of spec.tests) {
|
|
139252
139265
|
log(config, "debug", `TEST: ${test.testId}`);
|
|
139253
|
-
|
|
139266
|
+
metaValues.specs[spec.specId].tests[test.testId] ??= { contexts: {} };
|
|
139267
|
+
const testReport = {
|
|
139254
139268
|
testId: test.testId,
|
|
139255
139269
|
description: test.description,
|
|
139256
139270
|
contentPath: test.contentPath,
|
|
139257
139271
|
detectSteps: test.detectSteps,
|
|
139258
|
-
contexts:
|
|
139272
|
+
contexts: new Array(test.contexts.length)
|
|
139259
139273
|
};
|
|
139260
|
-
|
|
139261
|
-
|
|
139262
|
-
|
|
139263
|
-
|
|
139264
|
-
|
|
139265
|
-
|
|
139266
|
-
|
|
139267
|
-
|
|
139268
|
-
|
|
139269
|
-
|
|
139270
|
-
|
|
139271
|
-
|
|
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 {
|
|
139272
139306
|
}
|
|
139273
|
-
|
|
139274
|
-
|
|
139275
|
-
|
|
139276
|
-
|
|
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}`,
|
|
139277
139346
|
steps: []
|
|
139278
139347
|
};
|
|
139279
|
-
|
|
139280
|
-
|
|
139281
|
-
|
|
139282
|
-
|
|
139283
|
-
|
|
139284
|
-
|
|
139285
|
-
result: "SKIPPED",
|
|
139286
|
-
resultDescription: errorMessage
|
|
139287
|
-
};
|
|
139288
|
-
report.summary.contexts.skipped++;
|
|
139289
|
-
testReport.contexts.push(contextReport);
|
|
139290
|
-
continue;
|
|
139291
|
-
}
|
|
139292
|
-
let supportedContext = isSupportedContext({
|
|
139293
|
-
context,
|
|
139294
|
-
apps: availableApps,
|
|
139295
|
-
platform
|
|
139296
|
-
});
|
|
139297
|
-
let freshInstallRedetected = false;
|
|
139298
|
-
if (!supportedContext && context.platform === platform && // Mirror isSupportedContext's own guard: isDriverRequired iterates
|
|
139299
|
-
// context.steps, so a malformed context without a steps array would
|
|
139300
|
-
// otherwise crash the loop here instead of skipping cleanly.
|
|
139301
|
-
Array.isArray(context?.steps) && isDriverRequired({ test: context }) && requiredBrowserAssets(context.browser?.name).length > 0) {
|
|
139302
|
-
const firstAttempt = !installAttempts.has((context.browser?.name ?? "<none>").toLowerCase());
|
|
139303
|
-
const outcome = await ensureContextBrowserInstalled({
|
|
139304
|
-
browserName: context.browser?.name,
|
|
139305
|
-
config,
|
|
139306
|
-
installAttempts,
|
|
139307
|
-
deps: {
|
|
139308
|
-
ensureBrowser: (asset, options) => ensureBrowserInstalled(asset, options),
|
|
139309
|
-
log
|
|
139310
|
-
}
|
|
139311
|
-
});
|
|
139312
|
-
if (firstAttempt && (outcome === "installed" || outcome === "failed")) {
|
|
139313
|
-
freshInstallRedetected = true;
|
|
139314
|
-
clearAppCache(config);
|
|
139315
|
-
availableApps = await getAvailableApps({ config });
|
|
139316
|
-
runnerDetails.availableApps = availableApps;
|
|
139317
|
-
supportedContext = isSupportedContext({
|
|
139318
|
-
context,
|
|
139319
|
-
apps: availableApps,
|
|
139320
|
-
platform
|
|
139321
|
-
});
|
|
139322
|
-
}
|
|
139323
|
-
}
|
|
139324
|
-
if (!supportedContext) {
|
|
139325
|
-
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)}}`;
|
|
139326
|
-
log(config, freshInstallRedetected ? "warning" : "info", errorMessage);
|
|
139327
|
-
contextReport = {
|
|
139328
|
-
...contextReport,
|
|
139329
|
-
result: "SKIPPED",
|
|
139330
|
-
resultDescription: errorMessage
|
|
139331
|
-
};
|
|
139332
|
-
report.summary.contexts.skipped++;
|
|
139333
|
-
testReport.contexts.push(contextReport);
|
|
139334
|
-
continue;
|
|
139335
|
-
}
|
|
139336
|
-
log(config, "debug", `CONTEXT:
|
|
139337
|
-
${JSON.stringify(context, null, 2)}`);
|
|
139338
|
-
let driver;
|
|
139339
|
-
if (!context.steps) {
|
|
139340
|
-
context.steps = [];
|
|
139341
|
-
}
|
|
139342
|
-
const driverRequired = isDriverRequired({ test: context });
|
|
139343
|
-
if (driverRequired) {
|
|
139344
|
-
const combo = combinationKey(context);
|
|
139345
|
-
if (warmUpDecision(warmUpResults.get(combo)) === "skip") {
|
|
139346
|
-
const errorMessage = `Skipping context '${context.browser?.name}' on '${context.platform}': this context combination could not start a driver earlier in this run.`;
|
|
139347
|
-
log(config, "warning", errorMessage);
|
|
139348
|
-
contextReport = {
|
|
139349
|
-
...contextReport,
|
|
139350
|
-
result: "SKIPPED",
|
|
139351
|
-
resultDescription: errorMessage
|
|
139352
|
-
};
|
|
139353
|
-
report.summary.contexts.skipped++;
|
|
139354
|
-
testReport.contexts.push(contextReport);
|
|
139355
|
-
continue;
|
|
139356
|
-
}
|
|
139357
|
-
let caps = getDriverCapabilities({
|
|
139358
|
-
runnerDetails,
|
|
139359
|
-
name: context.browser.name,
|
|
139360
|
-
options: {
|
|
139361
|
-
width: context.browser?.window?.width || 1200,
|
|
139362
|
-
height: context.browser?.window?.height || 800,
|
|
139363
|
-
headless: context.browser?.headless !== false
|
|
139364
|
-
}
|
|
139365
|
-
});
|
|
139366
|
-
log(config, "debug", "CAPABILITIES:");
|
|
139367
|
-
log(config, "debug", caps);
|
|
139368
|
-
if (appiumPort === void 0) {
|
|
139369
|
-
throw new Error("Driver requested but Appium was not started. isAppiumRequired(specs) and isDriverRequired(context) disagreed; this is a bug.");
|
|
139370
|
-
}
|
|
139371
|
-
try {
|
|
139372
|
-
driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
|
|
139373
|
-
} catch (error) {
|
|
139374
|
-
try {
|
|
139375
|
-
log(config, "warning", `Failed to start context '${context.browser?.name}' on '${platform}'. Retrying as headless.`);
|
|
139376
|
-
context.browser.headless = true;
|
|
139377
|
-
caps = getDriverCapabilities({
|
|
139378
|
-
runnerDetails,
|
|
139379
|
-
name: context.browser.name,
|
|
139380
|
-
options: {
|
|
139381
|
-
width: context.browser?.window?.width || 1200,
|
|
139382
|
-
height: context.browser?.window?.height || 800,
|
|
139383
|
-
headless: context.browser?.headless !== false
|
|
139384
|
-
}
|
|
139385
|
-
});
|
|
139386
|
-
driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
|
|
139387
|
-
} catch (error2) {
|
|
139388
|
-
let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`;
|
|
139389
|
-
if (context.browser?.name === "safari" || context.browser?.name === "webkit")
|
|
139390
|
-
errorMessage = errorMessage + " Make sure you've run `safaridriver --enable` in a terminal and enabled 'Allow Remote Automation' in Safari's Develop menu.";
|
|
139391
|
-
log(config, "error", errorMessage);
|
|
139392
|
-
if (!warmUpResults.has(combo))
|
|
139393
|
-
warmUpResults.set(combo, "failed");
|
|
139394
|
-
contextReport = {
|
|
139395
|
-
...contextReport,
|
|
139396
|
-
result: "SKIPPED",
|
|
139397
|
-
resultDescription: errorMessage
|
|
139398
|
-
};
|
|
139399
|
-
report.summary.contexts.skipped++;
|
|
139400
|
-
testReport.contexts.push(contextReport);
|
|
139401
|
-
continue;
|
|
139402
|
-
}
|
|
139403
|
-
}
|
|
139404
|
-
if (!warmUpResults.has(combo))
|
|
139405
|
-
warmUpResults.set(combo, "ok");
|
|
139406
|
-
if (context.browser?.viewport?.width || context.browser?.viewport?.height) {
|
|
139407
|
-
await setViewportSize(context, driver);
|
|
139408
|
-
} else if (context.browser?.window?.width || context.browser?.window?.height) {
|
|
139409
|
-
const windowSize = await driver.getWindowSize();
|
|
139410
|
-
await driver.setWindowSize(context.browser?.window?.width || windowSize.width, context.browser?.window?.height || windowSize.height);
|
|
139411
|
-
}
|
|
139412
|
-
}
|
|
139413
|
-
let stepExecutionFailed = false;
|
|
139414
|
-
for (let step of context.steps) {
|
|
139415
|
-
if (!step.stepId)
|
|
139416
|
-
step.stepId = (0, import_node_crypto5.randomUUID)();
|
|
139417
|
-
log(config, "debug", `STEP:
|
|
139418
|
-
${JSON.stringify(step, null, 2)}`);
|
|
139419
|
-
if (step.unsafe && runnerDetails.allowUnsafeSteps === false) {
|
|
139420
|
-
log(config, "warning", `Skipping unsafe step: ${step.description} in test ${test.testId} context ${context.contextId}`);
|
|
139421
|
-
const stepReport2 = {
|
|
139422
|
-
...step,
|
|
139423
|
-
result: "SKIPPED",
|
|
139424
|
-
resultDescription: "Skipped because unsafe steps aren't allowed."
|
|
139425
|
-
};
|
|
139426
|
-
contextReport.steps.push(stepReport2);
|
|
139427
|
-
report.summary.steps.skipped++;
|
|
139428
|
-
continue;
|
|
139429
|
-
}
|
|
139430
|
-
if (stepExecutionFailed) {
|
|
139431
|
-
const stepReport2 = {
|
|
139432
|
-
...step,
|
|
139433
|
-
result: "SKIPPED",
|
|
139434
|
-
resultDescription: "Skipped due to previous failure in context."
|
|
139435
|
-
};
|
|
139436
|
-
contextReport.steps.push(stepReport2);
|
|
139437
|
-
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)
|
|
139438
139354
|
continue;
|
|
139355
|
+
for (const stepReport of contextReport.steps) {
|
|
139356
|
+
report.summary.steps[stepReport.result.toLowerCase()]++;
|
|
139439
139357
|
}
|
|
139440
|
-
|
|
139441
|
-
const stepResult = await runStep({
|
|
139442
|
-
config,
|
|
139443
|
-
context,
|
|
139444
|
-
step,
|
|
139445
|
-
driver,
|
|
139446
|
-
metaValues,
|
|
139447
|
-
options: {
|
|
139448
|
-
openApiDefinitions: context.openApi || []
|
|
139449
|
-
}
|
|
139450
|
-
});
|
|
139451
|
-
log(config, "debug", `RESULT: ${stepResult.status}
|
|
139452
|
-
${JSON.stringify(stepResult, null, 2)}`);
|
|
139453
|
-
stepResult.result = stepResult.status;
|
|
139454
|
-
stepResult.resultDescription = stepResult.description;
|
|
139455
|
-
delete stepResult.status;
|
|
139456
|
-
delete stepResult.description;
|
|
139457
|
-
const stepReport = {
|
|
139458
|
-
...step,
|
|
139459
|
-
...stepResult
|
|
139460
|
-
};
|
|
139461
|
-
contextReport.steps.push(stepReport);
|
|
139462
|
-
report.summary.steps[stepReport.result.toLowerCase()]++;
|
|
139463
|
-
if (stepReport.result === "FAIL") {
|
|
139464
|
-
stepExecutionFailed = true;
|
|
139465
|
-
}
|
|
139466
|
-
}
|
|
139467
|
-
if (config.recording) {
|
|
139468
|
-
const stopRecordStep = {
|
|
139469
|
-
stopRecord: true,
|
|
139470
|
-
description: "Stopping recording",
|
|
139471
|
-
stepId: (0, import_node_crypto5.randomUUID)()
|
|
139472
|
-
};
|
|
139473
|
-
const stepResult = await runStep({
|
|
139474
|
-
config,
|
|
139475
|
-
context,
|
|
139476
|
-
step: stopRecordStep,
|
|
139477
|
-
driver,
|
|
139478
|
-
options: {
|
|
139479
|
-
openApiDefinitions: context.openApi || []
|
|
139480
|
-
}
|
|
139481
|
-
});
|
|
139482
|
-
stepResult.result = stepResult.status;
|
|
139483
|
-
stepResult.resultDescription = stepResult.description;
|
|
139484
|
-
delete stepResult.status;
|
|
139485
|
-
delete stepResult.description;
|
|
139486
|
-
const stepReport = {
|
|
139487
|
-
...stopRecordStep,
|
|
139488
|
-
...stepResult
|
|
139489
|
-
};
|
|
139490
|
-
contextReport.steps.push(stepReport);
|
|
139491
|
-
report.summary.steps[stepReport.result.toLowerCase()]++;
|
|
139492
|
-
}
|
|
139493
|
-
let contextResult;
|
|
139494
|
-
if (contextReport.steps.find((step) => step.result === "FAIL"))
|
|
139495
|
-
contextResult = "FAIL";
|
|
139496
|
-
else if (contextReport.steps.find((step) => step.result === "WARNING"))
|
|
139497
|
-
contextResult = "WARNING";
|
|
139498
|
-
else if (contextReport.steps.length === contextReport.steps.filter((step) => step.result === "SKIPPED").length)
|
|
139499
|
-
contextResult = "SKIPPED";
|
|
139500
|
-
else
|
|
139501
|
-
contextResult = "PASS";
|
|
139502
|
-
contextReport = { result: contextResult, ...contextReport };
|
|
139503
|
-
testReport.contexts.push(contextReport);
|
|
139504
|
-
report.summary.contexts[contextResult.toLowerCase()]++;
|
|
139505
|
-
if (driverRequired) {
|
|
139506
|
-
try {
|
|
139507
|
-
await driver.deleteSession();
|
|
139508
|
-
} catch (error) {
|
|
139509
|
-
log(config, "error", `Failed to delete driver session: ${error.message}`);
|
|
139510
|
-
}
|
|
139358
|
+
report.summary.contexts[contextReport.result.toLowerCase()]++;
|
|
139511
139359
|
}
|
|
139360
|
+
testReport.result = rollUpResults(testReport.contexts.filter(Boolean));
|
|
139361
|
+
report.summary.tests[testReport.result.toLowerCase()]++;
|
|
139512
139362
|
}
|
|
139513
|
-
|
|
139514
|
-
|
|
139515
|
-
testResult = "FAIL";
|
|
139516
|
-
else if (testReport.contexts.find((context) => context.result === "WARNING"))
|
|
139517
|
-
testResult = "WARNING";
|
|
139518
|
-
else if (testReport.contexts.length === testReport.contexts.filter((context) => context.result === "SKIPPED").length)
|
|
139519
|
-
testResult = "SKIPPED";
|
|
139520
|
-
else
|
|
139521
|
-
testResult = "PASS";
|
|
139522
|
-
testReport = { result: testResult, ...testReport };
|
|
139523
|
-
specReport.tests.push(testReport);
|
|
139524
|
-
report.summary.tests[testResult.toLowerCase()]++;
|
|
139363
|
+
specReport.result = rollUpResults(specReport.tests);
|
|
139364
|
+
report.summary.specs[specReport.result.toLowerCase()]++;
|
|
139525
139365
|
}
|
|
139526
|
-
|
|
139527
|
-
|
|
139528
|
-
|
|
139529
|
-
|
|
139530
|
-
|
|
139531
|
-
|
|
139532
|
-
|
|
139533
|
-
else
|
|
139534
|
-
specResult = "PASS";
|
|
139535
|
-
specReport = { result: specResult, ...specReport };
|
|
139536
|
-
report.specs.push(specReport);
|
|
139537
|
-
report.summary.specs[specResult.toLowerCase()]++;
|
|
139538
|
-
}
|
|
139539
|
-
if (appium) {
|
|
139540
|
-
log(config, "debug", "Closing Appium server");
|
|
139541
|
-
try {
|
|
139542
|
-
(0, import_tree_kill.default)(appium.pid);
|
|
139543
|
-
} 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
|
+
}
|
|
139544
139373
|
}
|
|
139545
139374
|
}
|
|
139546
139375
|
const herettoConfigs = config?.integrations?.heretto || [];
|
|
@@ -139566,6 +139395,330 @@ ${JSON.stringify(stepResult, null, 2)}`);
|
|
|
139566
139395
|
}
|
|
139567
139396
|
return report;
|
|
139568
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
|
+
}
|
|
139569
139722
|
async function runStep({ config = {}, context = {}, step, driver, metaValues = {}, options = {} }) {
|
|
139570
139723
|
let actionResult;
|
|
139571
139724
|
step = replaceEnvs(step);
|
|
@@ -139616,7 +139769,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
139616
139769
|
step,
|
|
139617
139770
|
driver
|
|
139618
139771
|
});
|
|
139619
|
-
|
|
139772
|
+
driver.state.recording = actionResult.recording ?? null;
|
|
139620
139773
|
} else if (typeof step.runCode !== "undefined") {
|
|
139621
139774
|
actionResult = await runCode({ config, step });
|
|
139622
139775
|
} else if (typeof step.runShell !== "undefined") {
|
|
@@ -139641,7 +139794,7 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
139641
139794
|
description: `Unknown step action: ${JSON.stringify(step)}`
|
|
139642
139795
|
};
|
|
139643
139796
|
}
|
|
139644
|
-
if (
|
|
139797
|
+
if (driver?.state?.recording) {
|
|
139645
139798
|
const currentUrl = await driver.getUrl();
|
|
139646
139799
|
if (currentUrl !== driver.state.url) {
|
|
139647
139800
|
driver.state.url = currentUrl;
|
|
@@ -139663,6 +139816,33 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
139663
139816
|
}
|
|
139664
139817
|
return actionResult;
|
|
139665
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
|
+
}
|
|
139666
139846
|
async function appiumIsReady(port, timeoutMs = 12e4) {
|
|
139667
139847
|
let isReady = false;
|
|
139668
139848
|
const start = Date.now();
|
|
@@ -139681,6 +139861,7 @@ async function appiumIsReady(port, timeoutMs = 12e4) {
|
|
|
139681
139861
|
return isReady;
|
|
139682
139862
|
}
|
|
139683
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;
|
|
139684
139865
|
const wdio = await loadHeavyDep("webdriverio", { ctx });
|
|
139685
139866
|
let lastError;
|
|
139686
139867
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -139697,11 +139878,11 @@ async function driverStart(capabilities, port, maxAttempts = 4, ctx = {}) {
|
|
|
139697
139878
|
waitforTimeout: 12e4
|
|
139698
139879
|
// 2 minutes
|
|
139699
139880
|
});
|
|
139700
|
-
driver.state = { url: "", x: null, y: null };
|
|
139881
|
+
driver.state = { url: "", x: null, y: null, recording: null };
|
|
139701
139882
|
return driver;
|
|
139702
139883
|
} catch (err) {
|
|
139703
139884
|
lastError = err;
|
|
139704
|
-
if (
|
|
139885
|
+
if (!TRANSIENT.test(String(err && err.message)))
|
|
139705
139886
|
throw err;
|
|
139706
139887
|
if (attempt < maxAttempts) {
|
|
139707
139888
|
await new Promise((r) => setTimeout(r, 500 * attempt));
|