@wdio/cli
Version:
WebdriverIO testrunner command line interface
3,275 lines • 121 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/index.ts
import "dotenv/config";
// src/launcher.ts
import exitHook from "async-exit-hook";
import { resolve as resolve2 } from "import-meta-resolve";
import logger3 from "@wdio/logger";
import { validateConfig } from "@wdio/config";
import { ConfigParser as ConfigParser2 } from "@wdio/config/node";
import { initializePlugin, initializeLauncherService, sleep, enableFileLogging } from "@wdio/utils";
import { setupDriver, setupBrowser } from "@wdio/utils/node";
// src/interface.ts
import { EventEmitter } from "node:events";
import chalk2, { supportsColor } from "chalk";
import logger2 from "@wdio/logger";
import { SnapshotManager } from "@vitest/snapshot/manager";
// src/utils.ts
import fs2 from "node:fs/promises";
import util, { promisify } from "node:util";
import path2, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { execSync, spawn } from "node:child_process";
import ejs from "ejs";
import chalk from "chalk";
import inquirer from "inquirer";
import pickBy from "lodash.pickby";
import logger from "@wdio/logger";
import readDir from "recursive-readdir";
import { $ } from "execa";
import { readPackageUp } from "read-pkg-up";
import { resolve } from "import-meta-resolve";
import { SevereServiceError } from "webdriverio";
import { ConfigParser } from "@wdio/config/node";
import { CAPABILITY_KEYS } from "@wdio/protocols";
// src/install.ts
import { execa } from "execa";
var installCommand = {
npm: "install",
pnpm: "add",
yarn: "add",
bun: "install"
};
var devFlag = {
npm: "--save-dev",
pnpm: "--save-dev",
yarn: "--dev",
bun: "--dev"
};
async function installPackages(cwd, packages, dev) {
const pm = detectPackageManager();
const devParam = dev ? devFlag[pm] : "";
console.log("\n");
const p = execa(pm, [installCommand[pm], ...packages, devParam], {
cwd,
stdout: process.stdout,
stderr: process.stderr
});
const { stdout, stderr, exitCode } = await p;
if (exitCode !== 0) {
const cmd = getInstallCommand(pm, packages, dev);
const customError = `\u26A0\uFE0F An unknown error happened! Please retry installing dependencies via "${cmd}"
Error: ${stderr || stdout || "unknown"}`;
console.error(customError);
return false;
}
return true;
}
function getInstallCommand(pm, packages, dev) {
const devParam = dev ? devFlag[pm] : "";
return `${pm} ${installCommand[pm]} ${packages.join(" ")} ${devParam}`;
}
// src/constants.ts
import fs from "node:fs";
import path from "node:path";
import module from "node:module";
import { HOOK_DEFINITION } from "@wdio/utils";
var require2 = module.createRequire(import.meta.url);
var pkgJSON = require2("../package.json");
var pkg = pkgJSON;
var CLI_EPILOGUE = `Documentation: https://webdriver.io
@wdio/cli (v${pkg.version})`;
var CONFIG_HELPER_INTRO = `
===============================
\u{1F916} WDIO Configuration Wizard \u{1F9D9}
===============================
`;
var SUPPORTED_COMMANDS = ["run", "install", "config", "repl"];
var PMs = ["npm", "yarn", "pnpm", "bun"];
var SUPPORTED_CONFIG_FILE_EXTENSION = ["js", "ts", "mjs", "mts", "cjs", "cts"];
var configHelperSuccessMessage = ({ projectRootDir, runScript, extraInfo = "" }) => `
\u{1F916} Successfully setup project at ${projectRootDir} \u{1F389}
Join our Discord Community Server and instantly find answers to your issues or queries. Or just join and say hi \u{1F44B}!
\u{1F517} https://discord.webdriver.io
Visit the project on GitHub to report bugs \u{1F41B} or raise feature requests \u{1F4A1}:
\u{1F517} https://github.com/webdriverio/webdriverio
${extraInfo}
To run your tests, execute:
$ cd ${projectRootDir}
$ npm run ${runScript}
`;
var CONFIG_HELPER_SERENITY_BANNER = `
Learn more about Serenity/JS:
\u{1F517} https://serenity-js.org/
\u{1F517} https://serenity-js.org/handbook/test-runners/webdriverio/
`;
var DEPENDENCIES_INSTALLATION_MESSAGE = `
To install dependencies, execute:
%s
`;
var ANDROID_CONFIG = {
platformName: "Android",
automationName: "UiAutomator2",
deviceName: "Test"
};
var IOS_CONFIG = {
platformName: "iOS",
automationName: "XCUITest",
deviceName: "iPhone Simulator"
};
var SUPPORTED_PACKAGES = {
runner: [
{ name: "E2E Testing - of Web or Mobile Applications", value: "@wdio/local-runner$--$local$--$e2e" },
{ name: "Component or Unit Testing - in the browser\n > https://webdriver.io/docs/component-testing", value: "@wdio/browser-runner$--$browser$--$component" },
{ name: "Desktop Testing - of Electron Applications\n > https://webdriver.io/docs/desktop-testing/electron", value: "@wdio/local-runner$--$local$--$electron" },
{ name: "Desktop Testing - of MacOS Applications\n > https://webdriver.io/docs/desktop-testing/macos", value: "@wdio/local-runner$--$local$--$macos" },
{ name: "VS Code Extension Testing\n > https://webdriver.io/docs/vscode-extension-testing", value: "@wdio/local-runner$--$local$--$vscode" },
{ name: "Roku Testing - of OTT apps running on RokuOS\n > https://webdriver.io/docs/wdio-roku-service", value: "@wdio/local-runner$--$local$--$roku" }
],
framework: [
{ name: "Mocha (https://mochajs.org/)", value: "@wdio/mocha-framework$--$mocha" },
{ name: "Mocha with Serenity/JS (https://serenity-js.org/)", value: "@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha" },
{ name: "Jasmine (https://jasmine.github.io/)", value: "@wdio/jasmine-framework$--$jasmine" },
{ name: "Jasmine with Serenity/JS (https://serenity-js.org/)", value: "@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine" },
{ name: "Cucumber (https://cucumber.io/)", value: "@wdio/cucumber-framework$--$cucumber" },
{ name: "Cucumber with Serenity/JS (https://serenity-js.org/)", value: "@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber" }
],
reporter: [
{ name: "spec", value: "@wdio/spec-reporter$--$spec", checked: true },
{ name: "dot", value: "@wdio/dot-reporter$--$dot" },
{ name: "junit", value: "@wdio/junit-reporter$--$junit" },
{ name: "allure", value: "@wdio/allure-reporter$--$allure" },
{ name: "sumologic", value: "@wdio/sumologic-reporter$--$sumologic" },
{ name: "concise", value: "@wdio/concise-reporter$--$concise" },
{ name: "json", value: "@wdio/json-reporter$--$json" },
// external
{ name: "reportportal", value: "wdio-reportportal-reporter$--$reportportal" },
{ name: "video", value: "wdio-video-reporter$--$video" },
{ name: "cucumber-json", value: "wdio-cucumberjs-json-reporter$--$cucumberjs-json" },
{ name: "mochawesome", value: "wdio-mochawesome-reporter$--$mochawesome" },
{ name: "timeline", value: "wdio-timeline-reporter$--$timeline" },
{ name: "html-nice", value: "wdio-html-nice-reporter$--$html-nice" },
{ name: "slack", value: "@moroo/wdio-slack-reporter$--$slack" },
{ name: "teamcity", value: "wdio-teamcity-reporter$--$teamcity" },
{ name: "delta", value: "@delta-reporter/wdio-delta-reporter-service$--$delta" },
{ name: "testrail", value: "@wdio/testrail-reporter$--$testrail" },
{ name: "light", value: "wdio-light-reporter$--$light" },
{ name: "wdio-json-html-reporter", value: "wdio-json-html-reporter$--$jsonhtml" }
],
plugin: [
{ name: "wait-for: utilities that provide functionalities to wait for certain conditions till a defined task is complete.\n > https://www.npmjs.com/package/wdio-wait-for", value: "wdio-wait-for$--$wait-for" },
{ name: "angular-component-harnesses: support for Angular component test harnesses\n > https://www.npmjs.com/package/@badisi/wdio-harness", value: "@badisi/wdio-harness$--$harness" },
{ name: "Testing Library: utilities that encourage good testing practices laid down by dom-testing-library.\n > https://testing-library.com/docs/webdriverio-testing-library/intro", value: "@testing-library/webdriverio$--$testing-library" }
],
service: [
// internal or community driver services
{ name: "visual", value: "@wdio/visual-service$--$visual" },
{ name: "vite", value: "wdio-vite-service$--$vite" },
{ name: "nuxt", value: "wdio-nuxt-service$--$nuxt" },
{ name: "firefox-profile", value: "@wdio/firefox-profile-service$--$firefox-profile" },
{ name: "gmail", value: "wdio-gmail-service$--$gmail" },
{ name: "sauce", value: "@wdio/sauce-service$--$sauce" },
{ name: "testingbot", value: "@wdio/testingbot-service$--$testingbot" },
{ name: "browserstack", value: "@wdio/browserstack-service$--$browserstack" },
{ name: "lighthouse", value: "@wdio/lighthouse-service$--$lighthouse" },
{ name: "vscode", value: "wdio-vscode-service$--$vscode" },
{ name: "electron", value: "wdio-electron-service$--$electron" },
{ name: "appium", value: "@wdio/appium-service$--$appium" },
// external
{ name: "eslinter-service", value: "wdio-eslinter-service$--$eslinter" },
{ name: "lambdatest", value: "wdio-lambdatest-service$--$lambdatest" },
{ name: "zafira-listener", value: "wdio-zafira-listener-service$--$zafira-listener" },
{ name: "reportportal", value: "wdio-reportportal-service$--$reportportal" },
{ name: "docker", value: "wdio-docker-service$--$docker" },
{ name: "ui5", value: "wdio-ui5-service$--$ui5" },
{ name: "wiremock", value: "wdio-wiremock-service$--$wiremock" },
{ name: "ng-apimock", value: "wdio-ng-apimock-service$--$ng-apimock" },
{ name: "slack", value: "wdio-slack-service$--$slack" },
{ name: "cucumber-viewport-logger", value: "wdio-cucumber-viewport-logger-service$--$cucumber-viewport-logger" },
{ name: "intercept", value: "wdio-intercept-service$--$intercept" },
{ name: "docker", value: "wdio-docker-service$--$docker" },
{ name: "novus-visual-regression", value: "wdio-novus-visual-regression-service$--$novus-visual-regression" },
{ name: "rerun", value: "wdio-rerun-service$--$rerun" },
{ name: "winappdriver", value: "wdio-winappdriver-service$--$winappdriver" },
{ name: "ywinappdriver", value: "wdio-ywinappdriver-service$--$ywinappdriver" },
{ name: "performancetotal", value: "wdio-performancetotal-service$--$performancetotal" },
{ name: "cleanuptotal", value: "wdio-cleanuptotal-service$--$cleanuptotal" },
{ name: "aws-device-farm", value: "wdio-aws-device-farm-service$--$aws-device-farm" },
{ name: "ms-teams", value: "wdio-ms-teams-service$--$ms-teams" },
{ name: "tesults", value: "wdio-tesults-service$--$tesults" },
{ name: "azure-devops", value: "@gmangiapelo/wdio-azure-devops-service$--$azure-devops" },
{ name: "google-Chat", value: "wdio-google-chat-service$--$google-chat" },
{ name: "qmate-service", value: "@sap_oss/wdio-qmate-service$--$qmate-service" },
{ name: "robonut", value: "wdio-robonut-service$--$robonut" },
{ name: "qunit", value: "wdio-qunit-service$--$qunit" },
{ name: "roku", value: "wdio-roku-service$--$roku" }
]
};
var SUPPORTED_BROWSER_RUNNER_PRESETS = [
{ name: "Lit (https://lit.dev/)", value: "$--$" },
{ name: "Vue.js (https://vuejs.org/)", value: "@vitejs/plugin-vue$--$vue" },
{ name: "Svelte (https://svelte.dev/)", value: "@sveltejs/vite-plugin-svelte$--$svelte" },
{ name: "SolidJS (https://www.solidjs.com/)", value: "vite-plugin-solid$--$solid" },
{ name: "StencilJS (https://stenciljs.com/)", value: "$--$stencil" },
{ name: "React (https://reactjs.org/)", value: "@vitejs/plugin-react$--$react" },
{ name: "Preact (https://preactjs.com/)", value: "@preact/preset-vite$--$preact" },
{ name: "Other", value: null }
];
var TESTING_LIBRARY_PACKAGES = {
react: "@testing-library/react",
preact: "@testing-library/preact",
vue: "@testing-library/vue",
svelte: "@testing-library/svelte",
solid: "solid-testing-library"
};
var BackendChoice = /* @__PURE__ */ ((BackendChoice2) => {
BackendChoice2["Local"] = "On my local machine";
BackendChoice2["Experitest"] = "In the cloud using Experitest";
BackendChoice2["Saucelabs"] = "In the cloud using Sauce Labs";
BackendChoice2["Browserstack"] = "In the cloud using BrowserStack";
BackendChoice2["OtherVendors"] = "In the cloud using Testingbot or LambdaTest or a different service";
BackendChoice2["Grid"] = "I have my own Selenium cloud";
return BackendChoice2;
})(BackendChoice || {});
var ElectronBuildToolChoice = /* @__PURE__ */ ((ElectronBuildToolChoice2) => {
ElectronBuildToolChoice2["ElectronForge"] = "Electron Forge (https://www.electronforge.io/)";
ElectronBuildToolChoice2["ElectronBuilder"] = "electron-builder (https://www.electron.build/)";
ElectronBuildToolChoice2["SomethingElse"] = "Something else";
return ElectronBuildToolChoice2;
})(ElectronBuildToolChoice || {});
var ProtocolOptions = /* @__PURE__ */ ((ProtocolOptions2) => {
ProtocolOptions2["HTTPS"] = "https";
ProtocolOptions2["HTTP"] = "http";
return ProtocolOptions2;
})(ProtocolOptions || {});
var RegionOptions = /* @__PURE__ */ ((RegionOptions2) => {
RegionOptions2["US"] = "us";
RegionOptions2["EU"] = "eu";
return RegionOptions2;
})(RegionOptions || {});
var E2E_ENVIRONMENTS = [
{ name: "Web - web applications in the browser", value: "web" },
{ name: "Mobile - native, hybrid and mobile web apps, on Android or iOS", value: "mobile" }
];
var MOBILE_ENVIRONMENTS = [
{ name: "Android - native, hybrid and mobile web apps, tested on emulators and real devices\n > using UiAutomator2 (https://www.npmjs.com/package/appium-uiautomator2-driver)", value: "android" },
{ name: "iOS - applications on iOS, iPadOS, and tvOS\n > using XCTest (https://appium.github.io/appium-xcuitest-driver)", value: "ios" }
];
var BROWSER_ENVIRONMENTS = [
{ name: "Chrome", value: "chrome", checked: true },
{ name: "Firefox", value: "firefox" },
{ name: "Safari", value: "safari" },
{ name: "Microsoft Edge", value: "MicrosoftEdge" }
];
function isBrowserRunner(answers) {
return answers.runner === SUPPORTED_PACKAGES.runner[1].value;
}
function usesSerenity(answers) {
return answers.framework.includes("serenity-js");
}
function getTestingPurpose(answers) {
return convertPackageHashToObject(answers.runner).purpose;
}
var isNuxtProject = [
path.join(process.cwd(), "nuxt.config.js"),
path.join(process.cwd(), "nuxt.config.ts"),
path.join(process.cwd(), "nuxt.config.mjs"),
path.join(process.cwd(), "nuxt.config.mts")
].map((p) => {
try {
fs.accessSync(p);
return true;
} catch {
return false;
}
}).some(Boolean);
function selectDefaultService(serviceNames) {
serviceNames = Array.isArray(serviceNames) ? serviceNames : [serviceNames];
return SUPPORTED_PACKAGES.service.filter(({ name }) => serviceNames.includes(name)).map(({ value }) => value);
}
function prioServiceOrderFor(serviceNamesParam) {
const serviceNames = Array.isArray(serviceNamesParam) ? serviceNamesParam : [serviceNamesParam];
let services = SUPPORTED_PACKAGES.service;
for (const serviceName of serviceNames) {
const index = services.findIndex(({ name }) => name === serviceName);
services = [services[index], ...services.slice(0, index), ...services.slice(index + 1)];
}
return services;
}
var QUESTIONNAIRE = [{
type: "list",
name: "runner",
message: "What type of testing would you like to do?",
choices: SUPPORTED_PACKAGES.runner
}, {
type: "list",
name: "preset",
message: "Which framework do you use for building components?",
choices: SUPPORTED_BROWSER_RUNNER_PRESETS,
// only ask if there are more than 1 runner to pick from
when: (
/* istanbul ignore next */
isBrowserRunner
)
}, {
type: "confirm",
name: "installTestingLibrary",
message: "Do you like to use Testing Library (https://testing-library.com/) as test utility?",
default: true,
// only ask if there are more than 1 runner to pick from
when: (
/* istanbul ignore next */
(answers) => isBrowserRunner(answers) && /**
* Only show if Testing Library has an add-on for framework
*/
answers.preset && TESTING_LIBRARY_PACKAGES[convertPackageHashToObject(answers.preset).short]
)
}, {
type: "list",
name: "electronBuildTool",
message: "Which tool are you using to build your Electron app?",
choices: Object.values(ElectronBuildToolChoice),
when: (
/* instanbul ignore next */
(answers) => getTestingPurpose(answers) === "electron"
)
}, {
type: "input",
name: "electronAppBinaryPath",
message: "What is the path to the binary of your built Electron app?",
when: (
/* istanbul ignore next */
(answers) => getTestingPurpose(answers) === "electron" && answers.electronBuildTool === "Something else" /* SomethingElse */
)
}, {
type: "list",
name: "backend",
message: "Where is your automation backend located?",
choices: Object.values(BackendChoice),
when: (
/* instanbul ignore next */
(answers) => getTestingPurpose(answers) === "e2e"
)
}, {
type: "list",
name: "e2eEnvironment",
message: "Which environment you would like to automate?",
choices: E2E_ENVIRONMENTS,
default: "web",
when: (
/* istanbul ignore next */
(answers) => getTestingPurpose(answers) === "e2e"
)
}, {
type: "list",
name: "mobileEnvironment",
message: "Which mobile environment you'd like to automate?",
choices: MOBILE_ENVIRONMENTS,
when: (
/* instanbul ignore next */
(answers) => getTestingPurpose(answers) === "e2e" && answers.e2eEnvironment === "mobile"
)
}, {
type: "checkbox",
name: "browserEnvironment",
message: "With which browser should we start?",
choices: BROWSER_ENVIRONMENTS,
when: (
/* instanbul ignore next */
(answers) => getTestingPurpose(answers) === "e2e" && answers.e2eEnvironment === "web"
)
}, {
type: "input",
name: "hostname",
message: "What is the host address of that cloud service?",
when: (
/* istanbul ignore next */
(answers) => answers.backend && answers.backend.indexOf("different service") > -1
)
}, {
type: "input",
name: "port",
message: "What is the port on which that service is running?",
default: "80",
when: (
/* istanbul ignore next */
(answers) => answers.backend && answers.backend.indexOf("different service") > -1
)
}, {
type: "input",
name: "expEnvAccessKey",
message: "Access key from Experitest Cloud",
default: "EXPERITEST_ACCESS_KEY",
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Experitest" /* Experitest */
)
}, {
type: "input",
name: "expEnvHostname",
message: "Environment variable for cloud url",
default: "example.experitest.com",
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Experitest" /* Experitest */
)
}, {
type: "input",
name: "expEnvPort",
message: "Environment variable for port",
default: "443",
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Experitest" /* Experitest */
)
}, {
type: "list",
name: "expEnvProtocol",
message: "Choose a protocol for environment variable",
default: "https" /* HTTPS */,
choices: Object.values(ProtocolOptions),
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Experitest" /* Experitest */ && answers.expEnvPort !== "80" && answers.expEnvPort !== "443"
)
}, {
type: "input",
name: "env_user",
message: "Environment variable for username",
default: "LT_USERNAME",
when: (
/* istanbul ignore next */
(answers) => answers.backend && answers.backend.indexOf("LambdaTest") > -1 && answers.hostname.indexOf("lambdatest.com") > -1
)
}, {
type: "input",
name: "env_key",
message: "Environment variable for access key",
default: "LT_ACCESS_KEY",
when: (
/* istanbul ignore next */
(answers) => answers.backend && answers.backend.indexOf("LambdaTest") > -1 && answers.hostname.indexOf("lambdatest.com") > -1
)
}, {
type: "input",
name: "env_user",
message: "Environment variable for username",
default: "BROWSERSTACK_USERNAME",
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using BrowserStack" /* Browserstack */
)
}, {
type: "input",
name: "env_key",
message: "Environment variable for access key",
default: "BROWSERSTACK_ACCESS_KEY",
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using BrowserStack" /* Browserstack */
)
}, {
type: "input",
name: "env_user",
message: "Environment variable for username",
default: "SAUCE_USERNAME",
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */
)
}, {
type: "input",
name: "env_key",
message: "Environment variable for access key",
default: "SAUCE_ACCESS_KEY",
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */
)
}, {
type: "list",
name: "region",
message: "In which region do you want to run your Sauce Labs tests in?",
choices: Object.values(RegionOptions),
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */
)
}, {
type: "confirm",
name: "useSauceConnect",
message: "Are you testing a local application and need Sauce Connect to be set-up?\nRead more on Sauce Connect at: https://docs.saucelabs.com/secure-connections/#sauce-connect-proxy",
default: isNuxtProject,
when: (
/* istanbul ignore next */
(answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */ && !isNuxtProject
)
}, {
type: "input",
name: "hostname",
message: "What is the IP or URI to your Selenium standalone or grid server?",
default: "localhost",
when: (
/* istanbul ignore next */
(answers) => answers.backend && answers.backend.toString().indexOf("own Selenium cloud") > -1
)
}, {
type: "input",
name: "port",
message: "What is the port which your Selenium standalone or grid server is running on?",
default: "4444",
when: (
/* istanbul ignore next */
(answers) => answers.backend && answers.backend.toString().indexOf("own Selenium cloud") > -1
)
}, {
type: "input",
name: "path",
message: "What is the path to your browser driver or grid server?",
default: "/",
when: (
/* istanbul ignore next */
(answers) => answers.backend && answers.backend.toString().indexOf("own Selenium cloud") > -1
)
}, {
type: "list",
name: "framework",
message: "Which framework do you want to use?",
choices: (
/* instanbul ignore next */
(answers) => {
if (isBrowserRunner(answers)) {
return SUPPORTED_PACKAGES.framework.slice(0, 1);
}
if (getTestingPurpose(answers) === "electron") {
return SUPPORTED_PACKAGES.framework.filter(
({ value }) => !value.startsWith("@serenity-js")
);
}
return SUPPORTED_PACKAGES.framework;
}
)
}, {
type: "confirm",
name: "isUsingTypeScript",
message: "Do you want to use Typescript to write tests?",
when: (
/* istanbul ignore next */
(answers) => {
if (answers.preset?.includes("stencil")) {
return false;
}
return true;
}
),
default: (
/* istanbul ignore next */
(answers) => answers.preset?.includes("stencil") || detectCompiler(answers)
)
}, {
type: "confirm",
name: "generateTestFiles",
message: "Do you want WebdriverIO to autogenerate some test files?",
default: true,
when: (
/* istanbul ignore next */
(answers) => {
if (["vscode", "electron", "macos"].includes(getTestingPurpose(answers)) && answers.framework.includes("cucumber")) {
return false;
}
return true;
}
)
}, {
type: "input",
name: "specs",
message: "What should be the location of your spec files?",
default: (
/* istanbul ignore next */
(answers) => {
const pattern = isBrowserRunner(answers) ? "src/**/*.test" : "test/specs/**/*";
return getDefaultFiles(answers, pattern);
}
),
when: (
/* istanbul ignore next */
(answers) => answers.generateTestFiles && /(mocha|jasmine)/.test(answers.framework)
)
}, {
type: "input",
name: "specs",
message: "What should be the location of your feature files?",
default: (answers) => getDefaultFiles(answers, "features/**/*.feature"),
when: (
/* istanbul ignore next */
(answers) => answers.generateTestFiles && answers.framework.includes("cucumber")
)
}, {
type: "input",
name: "stepDefinitions",
message: "What should be the location of your step definitions?",
default: (answers) => getDefaultFiles(answers, "features/step-definitions/steps"),
when: (
/* istanbul ignore next */
(answers) => answers.generateTestFiles && answers.framework.includes("cucumber")
)
}, {
type: "confirm",
name: "usePageObjects",
message: "Do you want to use page objects (https://martinfowler.com/bliki/PageObject.html)?",
default: true,
when: (
/* istanbul ignore next */
(answers) => answers.generateTestFiles && /**
* page objects aren't common for component testing
*/
!isBrowserRunner(answers) && /**
* and also not needed when running VS Code tests since the service comes with
* its own page object implementation, nor when running Electron or MacOS tests
*/
!["vscode", "electron", "macos"].includes(getTestingPurpose(answers)) && /**
* Serenity/JS generates Lean Page Objects by default, so there's no need to ask about it
* See https://serenity-js.org/handbook/web-testing/page-objects-pattern/
*/
!usesSerenity(answers)
)
}, {
type: "input",
name: "pages",
message: "Where are your page objects located?",
default: (
/* istanbul ignore next */
(answers) => answers.framework.match(/(mocha|jasmine)/) ? getDefaultFiles(answers, "test/pageobjects/**/*") : getDefaultFiles(answers, "features/pageobjects/**/*")
),
when: (
/* istanbul ignore next */
(answers) => answers.generateTestFiles && answers.usePageObjects
)
}, {
type: "input",
name: "serenityLibPath",
message: "What should be the location of your Serenity/JS Screenplay Pattern library?",
default: (
/* istanbul ignore next */
async (answers) => {
const projectRootDir = await getProjectRoot(answers);
const specsDir = path.resolve(projectRootDir, path.dirname(answers.specs || "").replace(/\*\*$/, ""));
return path.resolve(specsDir, "..", "serenity");
}
),
when: (
/* istanbul ignore next */
(answers) => answers.generateTestFiles && usesSerenity(answers)
)
}, {
type: "checkbox",
name: "reporters",
message: "Which reporter do you want to use?",
choices: SUPPORTED_PACKAGES.reporter
}, {
type: "checkbox",
name: "plugins",
message: "Do you want to add a plugin to your test setup?",
choices: SUPPORTED_PACKAGES.plugin,
default: []
}, {
type: "confirm",
name: "includeVisualTesting",
message: "Would you like to include Visual Testing to your setup? For more information see https://webdriver.io/docs/visual-testing!",
default: false,
when: (
/* istanbul ignore next */
(answers) => {
return ["e2e", "component"].includes(getTestingPurpose(answers));
}
)
}, {
type: "checkbox",
name: "services",
message: "Do you want to add a service to your test setup?",
choices: (answers) => {
const services = [];
if (answers.backend === "In the cloud using BrowserStack" /* Browserstack */) {
services.push("browserstack");
} else if (answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */) {
services.push("sauce");
}
if (answers.e2eEnvironment === "mobile") {
services.push("appium");
}
if (getTestingPurpose(answers) === "e2e" && isNuxtProject) {
services.push("nuxt");
}
if (getTestingPurpose(answers) === "vscode") {
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "vscode")];
} else if (getTestingPurpose(answers) === "electron") {
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "electron")];
} else if (getTestingPurpose(answers) === "macos") {
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "appium")];
} else if (getTestingPurpose(answers) === "roku") {
return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "roku")];
}
return prioServiceOrderFor(services);
},
default: (answers) => {
const defaultServices = [];
if (answers.backend === "In the cloud using BrowserStack" /* Browserstack */) {
defaultServices.push("browserstack");
} else if (answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */) {
defaultServices.push("sauce");
}
if (answers.e2eEnvironment === "mobile" || getTestingPurpose(answers) === "macos") {
defaultServices.push("appium");
}
if (getTestingPurpose(answers) === "vscode") {
defaultServices.push("vscode");
} else if (getTestingPurpose(answers) === "electron") {
defaultServices.push("electron");
} else if (getTestingPurpose(answers) === "roku") {
defaultServices.push("roku");
}
if (isNuxtProject) {
defaultServices.push("nuxt");
}
if (answers.includeVisualTesting) {
defaultServices.push("visual");
}
return selectDefaultService(defaultServices);
}
}, {
type: "input",
name: "outputDir",
message: "In which directory should the xunit reports get stored?",
default: "./",
when: (
/* istanbul ignore next */
(answers) => answers.reporters.includes("junit")
)
}, {
type: "input",
name: "outputDir",
message: "In which directory should the json reports get stored?",
default: "./",
when: (
/* istanbul ignore next */
(answers) => answers.reporters.includes("json")
)
}, {
type: "input",
name: "outputDir",
message: "In which directory should the mochawesome json reports get stored?",
default: "./",
when: (
/* istanbul ignore next */
(answers) => answers.reporters.includes("mochawesome")
)
}, {
type: "confirm",
name: "npmInstall",
message: () => `Do you want me to run \`${detectPackageManager()} install\``,
default: true
}];
var SUPPORTED_SNAPSHOTSTATE_OPTIONS = ["all", "new", "none"];
var COMMUNITY_PACKAGES_WITH_TS_SUPPORT = [
"wdio-electron-service",
"wdio-vscode-service",
"wdio-nuxt-service",
"wdio-vite-service",
"wdio-gmail-service",
"wdio-roku-service"
];
var TESTRUNNER_DEFAULTS = {
/**
* Define specs for test execution. You can either specify a glob
* pattern to match multiple files at once or wrap a glob or set of
* paths into an array to run them within a single worker process.
*/
specs: {
type: "object",
validate: (param) => {
if (!Array.isArray(param)) {
throw new Error('the "specs" option needs to be a list of strings');
}
}
},
/**
* exclude specs from test execution
*/
exclude: {
type: "object",
validate: (param) => {
if (!Array.isArray(param)) {
throw new Error('the "exclude" option needs to be a list of strings');
}
}
},
/**
* key/value definition of suites (named by key) and a list of specs as value
* to specify a specific set of tests to execute
*/
suites: {
type: "object"
},
/**
* Project root directory path.
*/
rootDir: {
type: "string"
},
/**
* If you only want to run your tests until a specific amount of tests have failed use
* bail (default is 0 - don't bail, run all tests).
*/
bail: {
type: "number",
default: 0
},
/**
* supported test framework by wdio testrunner
*/
framework: {
type: "string"
},
/**
* capabilities of WebDriver sessions
*/
capabilities: {
type: "object",
validate: (param) => {
if (!Array.isArray(param)) {
if (typeof param === "object") {
return true;
}
throw new Error('the "capabilities" options needs to be an object or a list of objects');
}
for (const option of param) {
if (typeof option === "object") {
continue;
}
throw new Error("expected every item of a list of capabilities to be of type object");
}
return true;
},
required: true
},
/**
* list of reporters to use, a reporter can be either a string or an object with
* reporter options, e.g.:
* [
* 'dot',
* {
* name: 'spec',
* outputDir: __dirname + '/reports'
* }
* ]
*/
reporters: {
type: "object",
validate: (param) => {
if (!Array.isArray(param)) {
throw new Error('the "reporters" options needs to be a list of strings');
}
const isValidReporter = (option) => typeof option === "string" || typeof option === "function";
for (const option of param) {
if (isValidReporter(option)) {
continue;
}
if (Array.isArray(option) && typeof option[1] === "object" && isValidReporter(option[0])) {
continue;
}
throw new Error(
'a reporter should be either a string in the format "wdio-<reportername>-reporter" or a function/class. Please see the docs for more information on custom reporters (https://webdriver.io/docs/customreporter)'
);
}
return true;
}
},
/**
* set of WDIO services to use
*/
services: {
type: "object",
validate: (param) => {
if (!Array.isArray(param)) {
throw new Error('the "services" options needs to be a list of strings and/or arrays');
}
for (const option of param) {
if (!Array.isArray(option)) {
if (typeof option === "string") {
continue;
}
throw new Error('the "services" options needs to be a list of strings and/or arrays');
}
}
return true;
},
default: []
},
/**
* Node arguments to specify when launching child processes
*/
execArgv: {
type: "object",
validate: (param) => {
if (!Array.isArray(param)) {
throw new Error('the "execArgv" options needs to be a list of strings');
}
},
default: []
},
/**
* amount of instances to be allowed to run in total
*/
maxInstances: {
type: "number"
},
/**
* amount of instances to be allowed to run per capability
*/
maxInstancesPerCapability: {
type: "number"
},
/**
* whether or not testrunner should inject `browser`, `$` and `$$` as
* global environment variables
*/
injectGlobals: {
type: "boolean"
},
/**
* Set to true if you want to update your snapshots.
*/
updateSnapshots: {
type: "string",
default: SUPPORTED_SNAPSHOTSTATE_OPTIONS[1],
validate: (param) => {
if (param && !SUPPORTED_SNAPSHOTSTATE_OPTIONS.includes(param)) {
throw new Error(`the "updateSnapshots" options needs to be one of "${SUPPORTED_SNAPSHOTSTATE_OPTIONS.join('", "')}"`);
}
}
},
/**
* Overrides default snapshot path. For example, to store snapshots next to test files.
*/
resolveSnapshotPath: {
type: "function",
validate: (param) => {
if (param && typeof param !== "function") {
throw new Error('the "resolveSnapshotPath" options needs to be a function');
}
}
},
/**
* The number of times to retry the entire specfile when it fails as a whole
*/
specFileRetries: {
type: "number",
default: 0
},
/**
* Delay in seconds between the spec file retry attempts
*/
specFileRetriesDelay: {
type: "number",
default: 0
},
/**
* Whether or not retried spec files should be retried immediately or deferred to the end of the queue
*/
specFileRetriesDeferred: {
type: "boolean",
default: true
},
/**
* whether or not print the log output grouped by test files
*/
groupLogsByTestSpec: {
type: "boolean",
default: false
},
/**
* list of strings to watch of `wdio` command is called with `--watch` flag
*/
filesToWatch: {
type: "object",
validate: (param) => {
if (!Array.isArray(param)) {
throw new Error('the "filesToWatch" option needs to be a list of strings');
}
}
},
shard: {
type: "object",
validate: (param) => {
if (typeof param !== "object") {
throw new Error('the "shard" options needs to be an object');
}
const p = param;
if (typeof p.current !== "number" || typeof p.total !== "number") {
throw new Error('the "shard" option needs to have "current" and "total" properties with number values');
}
if (p.current < 0 || p.current > p.total) {
throw new Error('the "shard.current" value has to be between 0 and "shard.total"');
}
}
},
/**
* hooks
*/
onPrepare: HOOK_DEFINITION,
onWorkerStart: HOOK_DEFINITION,
onWorkerEnd: HOOK_DEFINITION,
before: HOOK_DEFINITION,
beforeSession: HOOK_DEFINITION,
beforeSuite: HOOK_DEFINITION,
beforeHook: HOOK_DEFINITION,
beforeTest: HOOK_DEFINITION,
afterTest: HOOK_DEFINITION,
afterHook: HOOK_DEFINITION,
afterSuite: HOOK_DEFINITION,
afterSession: HOOK_DEFINITION,
after: HOOK_DEFINITION,
onComplete: HOOK_DEFINITION,
onReload: HOOK_DEFINITION,
beforeAssertion: HOOK_DEFINITION,
afterAssertion: HOOK_DEFINITION
};
var WORKER_GROUPLOGS_MESSAGES = {
normalExit: (cid) => `
***** List of steps of WorkerID=[${cid}] *****`,
exitWithError: (cid) => `
***** List of steps of WorkerID=[${cid}] that preceded the error above *****`
};
// src/templates/EjsHelpers.ts
var EjsHelpers = class {
useTypeScript;
useEsm;
constructor(config) {
this.useTypeScript = config.useTypeScript ?? false;
this.useEsm = config.useEsm ?? false;
}
if(condition, trueValue, falseValue = "") {
return condition ? trueValue : falseValue;
}
ifTs = (trueValue, falseValue = "") => this.if(this.useTypeScript, trueValue, falseValue);
ifEsm = (trueValue, falseValue = "") => this.if(this.useEsm, trueValue, falseValue);
param(name, type) {
return this.useTypeScript ? `${name}: ${type}` : name;
}
returns(type) {
return this.useTypeScript ? `: ${type}` : "";
}
import(exports, moduleId) {
const individualExports = exports.split(",").map((id) => id.trim());
const imports = this.useTypeScript ? individualExports : individualExports.filter((id) => !id.startsWith("type "));
if (!imports.length) {
return "";
}
const modulePath = this.modulePathFrom(moduleId);
return this.useEsm || this.useTypeScript ? `import { ${imports.join(", ")} } from '${modulePath}'` : `const { ${imports.join(", ")} } = require('${modulePath}')`;
}
modulePathFrom(moduleId) {
if (!(moduleId.startsWith(".") && this.useEsm)) {
return moduleId;
}
if (moduleId.endsWith("/") && this.useEsm) {
return moduleId + "index.js";
}
return moduleId + ".js";
}
export(keyword, name) {
if (this.useTypeScript) {
return `export ${keyword} ${name}`;
}
if (this.useEsm) {
return `export ${keyword} ${name}`;
}
if (["class", "function"].includes(keyword)) {
return `module.exports.${name} = ${keyword} ${name}`;
}
return `module.exports.${name}`;
}
};
// src/utils.ts
var log = logger("@wdio/cli:utils");
var __dirname = dirname(fileURLToPath(import.meta.url));
var NPM_COMMAND = /^win/.test(process.platform) ? "npm.cmd" : "npm";
var VERSION_REGEXP = /(\d+)\.(\d+)\.(\d+)-(alpha|beta|)\.(\d+)\+(.+)/g;
var TEMPLATE_ROOT_DIR = path2.join(__dirname, "templates", "exampleFiles");
var renderFile = promisify(ejs.renderFile);
var HookError = class extends SevereServiceError {
origin;
constructor(message, origin) {
super(message);
this.origin = origin;
}
};
async function runServiceHook(launcher, hookName, ...args) {
const start = Date.now();
return Promise.all(launcher.map(async (service) => {
try {
if (typeof service[hookName] === "function") {
await service[hookName](...args);
}
} catch (err) {
const message = `A service failed in the '${hookName}' hook
${err.stack}
`;
if (err instanceof SevereServiceError || err.name === "SevereServiceError") {
return { status: "rejected", reason: message, origin: hookName };
}
log.error(`${message}Continue...`);
}
})).then((results) => {
if (launcher.length) {
log.debug(`Finished to run "${hookName}" hook in ${Date.now() - start}ms`);
}
const rejectedHooks = results.filter((p) => p && p.status === "rejected");
if (rejectedHooks.length) {
return Promise.reject(new HookError(`
${rejectedHooks.map((p) => p && p.reason).join()}
Stopping runner...`, hookName));
}
});
}
async function runLauncherHook(hook, ...args) {
if (typeof hook === "function") {
hook = [hook];
}
const catchFn = (e) => {
log.error(`Error in hook: ${e.stack}`);
if (e instanceof SevereServiceError) {
throw new HookError(e.message, hook[0].name);
}
};
return Promise.all(hook.map((hook2) => {
try {
return hook2(...args);
} catch (err) {
return catchFn(err);
}
})).catch(catchFn);
}
async function runOnCompleteHook(onCompleteHook, config, capabilities, exitCode, results) {
if (typeof onCompleteHook === "function") {
onCompleteHook = [onCompleteHook];
}
return Promise.all(onCompleteHook.map(async (hook) => {
try {
await hook(exitCode, config, capabilities, results);
return 0;
} catch (err) {
log.error(`Error in onCompleteHook: ${err.stack}`);
if (err instanceof SevereServiceError) {
throw new HookError(err.message, "onComplete");
}
return 1;
}
}));
}
function getRunnerName(caps = {}) {
let runner = caps.browserName || caps.platformName || caps["appium:platformName"] || caps["appium:appPackage"] || caps["appium:appWaitActivity"] || caps["appium:app"];
if (!runner) {
runner = Object.values(caps).length === 0 || Object.values(caps).some((cap) => !cap.capabilities) ? "undefined" : "MultiRemote";
}
return runner;
}
function buildNewConfigArray(str, type, change) {
const newStr = str.split(`${type}s: `)[1].replace(/'/g, "");
const newArray = newStr.match(/(\w*)/gmi)?.filter((e) => !!e).concat([change]) || [];
return str.replace("// ", "").replace(
new RegExp(`(${type}s: )((.*\\s*)*)`),
`$1[${newArray.map((e) => `'${e}'`)}]`
);
}
function buildNewConfigString(str, type, change) {
return str.replace(new RegExp(`(${type}: )('\\w*')`), `$1'${change}'`);
}
function findInConfig(config, type) {
let regexStr = `[\\/\\/]*[\\s]*${type}s: [\\s]*\\[([\\s]*['|"]\\w*['|"],*)*[\\s]*\\]`;
if (type === "framework") {
regexStr = `[\\/\\/]*[\\s]*${type}: ([\\s]*['|"]\\w*['|"])`;
}
const regex = new RegExp(regexStr, "gmi");
return config.match(regex);
}
function replaceConfig(config, type, name) {
if (type === "framework") {
return buildNewConfigString(config, type, name);
}
const match = findInConfig(config, type);
if (!match || match.length === 0) {
return;
}
const text = match.pop() || "";
return config.replace(text, buildNewConfigArray(text, type, name));
}
function addServiceDeps(names, packages, update = false) {
if (names.some(({ short }) => short === "appium")) {
const result = execSync("appium --version || echo APPIUM_MISSING", { stdio: "pipe" }).toString().trim();
if (result === "APPIUM_MISSING") {
packages.push("appium");
} else if (update) {
console.log(
"\n=======",
"\nUsing globally installed appium",
result,
"\nPlease add the following to your wdio.conf.js:",
"\nappium: { command: 'appium' }",
"\n=======\n"
);
}
}
}
function convertPackageHashToObject(pkg2, hash = "$--$") {
const [p, short, purpose] = pkg2.split(hash);
return { package: p, short, purpose };
}
function getSerenityPackages(answers) {
const framework = convertPackageHashToObject(answers.framework);
if (framework.package !== "@serenity-js/webdriverio") {
return [];
}
const packages = {
cucumber: [
"@cucumber/cucumber",
"@serenity-js/cucumber"
],
mocha: [
"@serenity-js/mocha",
"mocha"
],
jasmine: [
"@serenity-js/jasmine",
"jasmine"
],
common: [
"@serenity-js/assertions",
"@serenity-js/console-reporter",
"@serenity-js/core",
"@serenity-js/rest",
"@serenity-js/serenity-bdd",
"@serenity-js/web",
"npm-failsafe",
"rimraf"
]
};
if (answers.isUsingTypeScript) {
packages.mocha.push("@types/mocha");
packages.jasmine.push("@types/jasmine");
packages.common.push("@types/node");
}
return [
...packages[framework.purpose],
...packages.common
].filter(Boolean).sort();
}
async function getCapabilities(arg) {
const optionalCapabilites = {
platformVersion: arg.platformVersion,
udid: arg.udid,
...arg.deviceName && { deviceName: arg.deviceName }
};
if (/.*\.(apk|app|ipa)$/.test(arg.option)) {
return {
capabilities: {
app: arg.option,
...arg.option.endsWith("apk") ? ANDROID_CONFIG : IOS_CONFIG,
...optionalCapabilites
}
};
} else if (/android/.test(arg.option)) {
return { capabilities: { browserName: "Chrome", ...ANDROID_CONFIG, ...optionalCapabilites } };
} else if (/ios/.test(arg.option)) {
return { capabilities: { browserName: "Safari", ...IOS_CONFIG, ...optionalCapabilites } };
} else if (/(js|ts)$/.test(arg.option)) {
const config = new ConfigParser(arg.option);
try {
await config.initialize();
} catch (e) {
throw Error(e.code === "MODULE_NOT_FOUND" ? `Config File not found: ${arg.option}` : `Could not parse ${arg.option}, failed with error: ${e.message}`);
}
if (typeof arg.capabilities === "undefined") {
throw Error("Please provide index/named property of capability to use from the capabilities array/object in wdio config file");
}
let requiredCaps = config.getCapabilities();
requiredCaps = // multi capabilities
requiredCaps[parseInt(arg.capabilities, 10)] || // multiremote
requiredCaps[arg.capabilities];
const requiredW3CCaps = pickBy(requiredCaps, (_, key) => CAPABILITY_KEYS.includes(key) || key.includes(":"));
if (!Object.keys(requiredW3CCaps).length) {
throw Error(`No capability found in given config file with the provided capability indexed/named property: ${arg.capabilities}. Please check the capability in your wdio config file.`);
}
return { capabilities: { ...requiredW3CCaps } };
}
return { capabilities: { browserName: arg.option } };
}
async function detectCompiler(answers) {
if (answers.createPackageJSON) {
return false;
}
const root = await getProjectRoot(answers);
const hasRootTSConfig = await fs2.access(path2.resolve(root, "tsconfig.json")).then(() => true, () => false);
return hasRootTSConfig;
}
async function generateTestFiles(answers) {
if (answers.serenityAdapter) {
return generateSerenityExamples(answers);
}
if (answers.runner === "local") {
return generateLocalRunnerTestFiles(answers);
}
return generateBrowserRunnerTestFiles(answers);
}
var TSX_BASED_FRAMEWORKS = ["react", "preact", "solid", "stencil"];
async function generateBrowserRunnerTestFiles(answers) {
const isUsingFramework = typeof answers.preset === "string";
const preset = getPreset(answers);
const tplRootDir = path2.join(TEMPLATE_ROOT_DIR, "browser");
await fs2.mkdir(answers.destSpecRootPath, { recursive: true });
if (isUsingFramework) {
const renderedCss = await renderFile(path2.join(tplRootDir, "Component.css.ejs"), { answers });
await fs2.writeFile(path2.join(answers.destSpecRootPath, "Component.css"), renderedCss);
}
const testExt = `${answers.isUsingTypeScript ? "ts" : "js"}${TSX_BASED_FRAMEWORKS.includes(preset) ? "x" : ""}`;
const fileExt = ["svelte", "vue"].includes(preset) ? preset : testExt;
if (preset) {
const componentOutFileName = `Component.${fileExt}`;
const renderedComponent = await renderFile(path2.join(tplRootDir, `Component.${preset}.ejs`), { answers });
await fs2.writeFile(path2.join(answers.destSpecRootPath, componentOutFileName), renderedComponent);
}
const componentFileName = preset ? `Component.${preset}.test.ejs` : "standalone.test.ejs";
const renderedTest = await renderFile(path2.join(tplRootDir, componentFileName), { answers });
await fs2.writeFile(path2.join(answers.destSpecRootPath, `Component.test.${testExt}`), renderedTest);
}
async function generateLocalRunnerTestFiles(answers) {
const testFiles = answers.framework === "cucumber" ? [path2.join(TEMPLATE_ROOT_DIR, "cucumber")] : [path2.join(TEMPLATE_ROOT_DIR, "mochaJasmine")];
if (answers.usePageObjects) {
testFiles.push(path2.join(TEMPLATE_ROOT_DIR, "pageobjects"));
}
const files = (await Promise.all(testFiles.map((dirPath) => readDir(
dirPath,
[(file, stats) => !stats.isDirectory() && !(file.endsWith(".ejs") || file.endsWith(".feature"))]
)))).reduce((cur, acc) => [...acc, ...cur], []);
await Promise.all(files.map(async (file) => {
const renderedTpl = await renderFile(file, { answers });
const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset);
const fileEnding = (answers.isUsingTypeScript ? ".ts" : ".js") + (isJSX ? "x" : "");
const destPath = (file.endsWith("page.js.ejs") ? path2.join(answers.destPageObjectRootPath, path2.basename(file)) : file.includes("step_definition") ? path2.join(answers.destStepRootPath, path2.basename(file)) : path2.join(answers.destSpecRootPath, path2.basename(file))).replace(/\.ejs$/, "").replace(/\.js$/, fileEnding);
await fs2.mkdir(path2.dirname(destPath), { recursive: true });
await fs2.writeFile(destPath, renderedTpl);
}));
}
async function generateSerenityExamples(answers) {
const templateDirectories = Object.entries({
[answers.projectRootDir]: path2.join(TEMPLATE_ROOT_DIR, "serenity-js", "common", "config"),
[answers.destSpecRootPath]: path2.join(TEMPLATE_ROOT_DIR, "serenity-js", answers.serenityAdapter),
[answers.destSerenityLibRootPath]: path2.join(TEMPLATE_ROOT_DIR, "serenity-js", "common", "serenity")
});
await Promise.all(templateDirectories.map(async ([destinationRootDir, templateRootDir]) => {
const pathsToTemplates = await readDir(templateRootDir);
await Promise.all(pathsToTemplates.map(async (pathToTemplate) => {
const extension = answers.isUsingTypeScript ? ".ts" : ".js";
const destination = path2.join(destinationRootDir, path2.relative(templateRootDir, pathToTemplate)).replace(/\.ejs$/, "").replace(/\.ts$/, extension);
const contents = await renderFile(
pathToTemplate,
{ answers, _: new EjsHelpers({ useEsm: answers.esmSupport, useTypeScript: answers.isUsingTypeScript }) }
);
await fs2.mkdir(path2.dirname(destination), { recursive: true });
await fs2.writeFile(destination, contents);
}));
}));
}
async function getAnswers(yes) {
if (yes) {
const ignoredQuestions = ["e2eEnvironment"];
const filterdQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name));
const answers2 = {};
for (const question of filterdQuestionaire) {
if (question.when && !question.when(answers2)) {
continue;
}
Object.assign(answers2, {
[question.name]: typeof question.default !== "undefined" ? typeof question.default === "function" ? await question.default(answers2) : question.default : question.choices && question.choices.length ? typeof question.choices === "function" ? question.choices(answers2)[0].value ? question.choices(answers2)[0].value : question.choices(answers2)[0] : question.choices[0].value ? question.type === "checkbox" ? [question.choices[0].value] : question.choices[0].value : question.choices[0] : {}
});
}
answers2.isUsingTypeScript = await answers2.isUsingTypeScript;
answers2.specs = await answers2.specs;
answers2.pages = await answers2.pages;
return answers2;
}
const projectProps = await getProjectProps(process.cwd());
const isProjectExisting = Boolean(projectProps);
const nameInPackageJsonIsNotCreateWdioDefault = projectProps?.packageJson?.name !== "my-new-project";
const projectName = projectProps?.packageJson?.name ? ` named "${projectProps.packageJson.name}"` : "";
const projectRootQuestions = [
{
type: "confirm",
name: "createPackageJSON",
default: true,
message: `Couldn't find a package.json in "${process.cwd()}" or any of the parent directories, do you want to create one?`,
when: !isProjectExisting
},
{
type: "confirm",
name: "projectRootCorrect",
default: true,
message: `A project${projectName} was detected at "${projectProps?.path}", correct?`,
when: isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault
},
{
type: "input",
name: "projectRoot",
message: "What is the project root for your test project?",
default: projectProps?.path,
// only ask if there are more than 1 runner to pick from
when: (
/* istanbul ignore next */
(answers2) => isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault && !answers2.projectRootCorrect
)
}
];
const answers = await inquirer.prompt(projectRootQuestions);
if (answers.createPackageJSON) {
answers.projectRoot = process.cwd();
}
return inquirer.prompt(QUESTIONNAIRE, answers);
}
function generatePathfromAnswer(answers, projectRootDir) {
return path2.resolve(
projectRootDir,
path2.dirname(answers) === "." ? path2.resolve(answers) : path2.dirname(answers)
);
}
function getPathForFileGeneration(answers, projectRootDir) {
const specAnswer = answers.specs || "";
const stepDefinitionAnswer = answers.stepDefinitions || "";
const pageObjectAnswer = answers.pages || "";
const destSpecRootPath = generatePathfromAnswer(specAnswer, projectRootDir).replace(/\*\*$/, "");
const destStepRootPath = generatePathfromAnswer(stepDefinitionAnswer, projectRootDir);
const destPageObjectRootPath = answers.usePageObjects ? generatePathfromAnswer(pageObjectAnswer, projectRootDir).replace(/\*\*$/, "") : "";
const destSerenityLibRootPath = usesSerenity(answers) ? path2.resolve(projectRootDir, answers.serenityLibPath || "serenity") : "";
const relativePath = answers.generateTestFiles && answers.usePageObjects ? !(convertPackageHashToObject(answers.framework).short === "cucumber") ? path2.relative(destSpecRootPath, destPageObjectRootPath) : path2.relative(destStepRootPath, destPageObjectRootPath) : "";
return {
destSpecRootPath,
destStepRootPath,
destPageObjectRootPath,
destSerenityLibRootPath,
relativePath: relativePath.replaceAll(path2.sep, "/")
};
}
async function getDefaultFiles(answers, pattern) {
const rootdir = await getProjectRoot(answers);
const presetPackage = convertPackageHashToObject(answers.preset || "");
const isJSX = TSX_BASED_FRAMEWORKS.includes(presetPackage.short || "");
const val = pattern.endsWith(".feature") ? path2.join(rootdir, pattern) : answers?.isUsingTypeScript ? `${path2.join(rootdir, pattern)}.ts${isJSX ? "x" : ""}` : `${path2.join(rootdir, pattern)}.js${isJSX ? "x" : ""}`;
return val;
}
function specifyVersionIfNeeded(packagesToInstall, version, npmTag) {
const { value } = version.matchAll(VERSION_REGEXP).next();
const [major, minor, patch, tagName, build] = (value || []).slice(1, -1);
return packagesToInstall.map((p) => {
if (p.startsWith("@wdio") && p !== "@wdio/visual-service" || ["webdriver", "webdriverio"].includes(p)) {
const tag = major && npmTag === "latest" ? `^${major}.${minor}.${patch}-${tagName}.${build}` : npmTag;
return `${p}@${tag}`;
}
return p;
});
}
async function getProjectProps(cwd = process.cwd()) {
try {
const { packageJson, path: packageJsonPath } = await readPackageUp({ cwd }) || {};
if (!packageJson || !packageJsonPath) {
return void 0;
}
return {
esmSupported: packageJson.type === "module" || typeof packageJson.module === "string",
packageJson,
path: path2.dirname(packageJsonPath)
};
} catch {
return void 0;
}
}
function runProgram(command5, args, options) {
const child = spawn(command5, args, { stdio: "inherit", ...options });
return new Promise((resolve3, reject) => {
let error;
child.on("error", (e) => error = e);
child.on("close", (code) => {
if (code !== 0) {
return reject(new Error(
error && error.message || `Error calling: ${command5} ${args.join(" ")}`
));
}
resolve3();
});
});
}
async function createPackageJSON(parsedAnswers) {
const packageJsonExists = await fs2.access(path2.resolve(process.cwd(), "package.json")).then(() => true, () => false);
if (packageJsonExists) {
return;
}
if (parsedAnswers.createPackageJSON === false) {
if (!packageJsonExists) {
console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`);
return !process.env.WDIO_UNIT_TESTS && process.exit(0);
}
return;
}
if (parsedAnswers.createPackageJSON) {
console.log(`Creating a ${chalk.bold("package.json")} for the directory...`);
await fs2.writeFile(path2.resolve(process.cwd(), "package.json"), JSON.stringify({
name: "webdriverio-tests",
version: "0.0.0",
private: true,
license: "ISC",
type: "module",
dependencies: {},
devDependencies: {}
}, null, 2));
console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
}
}
var SEP = "\n- ";
async function npmInstall(parsedAnswers, npmTag) {
const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service));
const presetPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.preset || "");
if (parsedAnswers.installTestingLibrary && TESTING_LIBRARY_PACKAGES[presetPackage.short]) {
parsedAnswers.packagesToInstall.push(
TESTING_LIBRARY_PACKAGES[presetPackage.short],
"@testing-library/jest-dom"
);
}
if (presetPackage.short === "solid") {
parsedAnswers.packagesToInstall.push("solid-js");
}
if (parsedAnswers.includeVisualTesting) {
parsedAnswers.packagesToInstall.push("@wdio/visual-service");
}
const preset = getPreset(parsedAnswers);
if (preset === "lit") {
parsedAnswers.packagesToInstall.push("lit");
}
if (preset === "stencil") {
parsedAnswers.packagesToInstall.push("@stencil/core");
}
if (presetPackage.short === "react") {
parsedAnswers.packagesToInstall.push("react");
if (!parsedAnswers.installTestingLibrary) {
parsedAnswers.packagesToInstall.push("react-dom");
}
}
if (parsedAnswers.framework === "jasmine" && parsedAnswers.isUsingTypeScript) {
parsedAnswers.packagesToInstall.push("@types/jasmine");
}
if (parsedAnswers.purpose === "macos") {
parsedAnswers.packagesToInstall.push("appium-mac2-driver");
}
if (parsedAnswers.mobileEnvironment === "android") {
parsedAnswers.packagesToInstall.push("appium-uiautomator2-driver");
}
if (parsedAnswers.mobileEnvironment === "ios") {
parsedAnswers.packagesToInstall.push("appium-xcuitest-driver");
}
addServiceDeps(servicePackages, parsedAnswers.packagesToInstall);
parsedAnswers.packagesToInstall = specifyVersionIfNeeded(parsedAnswers.packagesToInstall, pkg.version, npmTag);
const cwd = await getProjectRoot(parsedAnswers);
const pm = detectPackageManager();
if (parsedAnswers.npmInstall) {
console.log(`Installing packages using ${pm}:${SEP}${parsedAnswers.packagesToInstall.join(SEP)}`);
const success = await installPackages(cwd, parsedAnswers.packagesToInstall, true);
if (success) {
console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
}
} else {
const installationCommand = getInstallCommand(pm, parsedAnswers.packagesToInstall, true);
console.log(util.format(DEPENDENCIES_INSTALLATION_MESSAGE, installationCommand));
}
}
function detectPackageManager() {
if (!process.env.npm_config_user_agent) {
return "npm";
}
const detectedPM = process.env.npm_config_user_agent.split("/")[0].toLowerCase();
const matchedPM = PMs.find((pm) => pm.toLowerCase() === detectedPM);
return matchedPM || "npm";
}
async function setupTypeScript(parsedAnswers) {
if (!parsedAnswers.isUsingTypeScript) {
return;
}
if (parsedAnswers.hasRootTSConfig) {
return;
}
console.log("Setting up TypeScript...");
const frameworkPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.framework);
const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service));
const serenityTypes = parsedAnswers.serenityAdapter === "jasmine" ? ["jasmine"] : [];
const types = [
"node",
...parsedAnswers.framework === "jasmine" ? ["jasmine"] : [],
"@wdio/globals/types",
...parsedAnswers.framework === "jasmine" ? ["expect-webdriverio/jasmine"] : ["expect-webdriverio"],
...parsedAnswers.serenityAdapter ? serenityTypes : [frameworkPackage.package],
...parsedAnswers.runner === "browser" ? ["@wdio/browser-runner"] : [],
...servicePackages.filter((service) => (
/**
* given that we know that all "official" services have
* typescript support we only include them
*/
service.package.startsWith("@wdio") || /**
* also include community maintained packages with known
* support for TypeScript
*/
COMMUNITY_PACKAGES_WITH_TS_SUPPORT.includes(service.package)
)).map((service) => service.package)
];
const preset = getPreset(parsedAnswers);
const config = {
compilerOptions: {
// compiler
moduleResolution: "node",
module: !parsedAnswers.esmSupport ? "commonjs" : "ESNext",
target: "es2022",
lib: ["es2022", "dom"],
types,
skipLibCheck: true,
// bundler
noEmit: true,
allowImportingTsExtensions: true,
resolveJsonModule: true,
isolatedModules: true,
// linting
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
noFallthroughCasesInSwitch: true,
...Object.assign(
preset === "lit" ? {
experimentalDecorators: true,
useDefineForClassFields: false
} : {},
preset === "react" ? {
jsx: "react-jsx"
} : {},
preset === "preact" ? {
jsx: "react-jsx",
jsxImportSource: "preact"
} : {},
preset === "solid" ? {
jsx: "preserve",
jsxImportSource: "solid-js"
} : {},
preset === "stencil" ? {
experimentalDecorators: true,
jsx: "react",
jsxFactory: "h",
jsxFragmentFactory: "Fragment"
} : {}
)
},
include: preset === "svelte" ? ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] : preset === "vue" ? ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] : ["test", "wdio.conf.ts"]
};
if (parsedAnswers.framework === "cucumber") {
config.include.push("features");
}
await fs2.mkdir(path2.dirname(parsedAnswers.tsConfigFilePath), { recursive: true });
await fs2.writeFile(
parsedAnswers.tsConfigFilePath,
JSON.stringify(config, null, 4)
);
console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
}
function getPreset(parsedAnswers) {
const isUsingFramework = typeof parsedAnswers.preset === "string";
return isUsingFramework ? parsedAnswers.preset || "lit" : "";
}
async function createWDIOConfig(parsedAnswers) {
try {
console.log("Creating a WebdriverIO config file...");
const tplPath = path2.resolve(__dirname, "templates", "wdio.conf.tpl.ejs");
const renderedTpl = await renderFile(tplPath, {
answers: parsedAnswers,
_: new EjsHelpers({ useEsm: parsedAnswers.esmSupport, useTypeScript: parsedAnswers.isUsingTypeScript })
});
await fs2.writeFile(parsedAnswers.wdioConfigPath, renderedTpl);
console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
if (parsedAnswers.generateTestFiles) {
console.log("Autogenerating test files...");
await generateTestFiles(parsedAnswers);
console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
}
} catch (err) {
throw new Error(`\u26A0\uFE0F Couldn't write config file: ${err.stack}`);
}
}
async function getProjectRoot(parsedAnswers) {
if (parsedAnswers?.createPackageJSON && parsedAnswers.projectRoot) {
return parsedAnswers.projectRoot;
}
const root = (await getProjectProps())?.path;
if (!root) {
throw new Error("Could not find project root directory with a package.json");
}
return !parsedAnswers || parsedAnswers.projectRootCorrect ? root : parsedAnswers.projectRoot || process.cwd();
}
async function createWDIOScript(parsedAnswers) {
const rootDir = await getProjectRoot(parsedAnswers);
const pathToWdioConfig = `./${path2.join(".", parsedAnswers.wdioConfigPath.replace(rootDir, ""))}`;
const wdioScripts = {
"wdio": `wdio run ${pathToWdioConfig}`
};
const serenityScripts = {
"serenity": "failsafe serenity:clean wdio serenity:report",
"serenity:clean": "rimraf target",
"wdio": `wdio run ${pathToWdioConfig}`,
"serenity:report": "serenity-bdd run"
};
const scripts = parsedAnswers.serenityAdapter ? serenityScripts : wdioScripts;
for (const [script, command5] of Object.entries(scripts)) {
const args = ["pkg", "set", `scripts.${script}=${command5}`];
try {
console.log(`Adding ${chalk.bold(`"${script}"`)} script to package.json`);
await runProgram(NPM_COMMAND, args, { cwd: parsedAnswers.projectRootDir });
} catch (err) {
const [preArgs, scriptPath] = args.join(" ").split("=");
console.error(
`\u26A0\uFE0F Couldn't add script to package.json: "${err.message}", you can add it manually by running:
${NPM_COMMAND} ${preArgs}="${scriptPath}"`
);
return false;
}
}
console.log(chalk.green(chalk.bold("\u2714 Success!")));
return true;
}
async function runAppiumInstaller(parsedAnswers) {
if (parsedAnswers.e2eEnvironment !== "mobile") {
return;
}
const answer = await inquirer.prompt({
name: "continueWithAppiumSetup",
message: "Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?",
type: "confirm",
default: true
});
if (!answer.continueWithAppiumSetup) {
return console.log(
"Ok! You can learn more about setting up mobile environments in the Appium docs at https://appium.io/docs/en/2.0/quickstart/"
);
}
return $({ stdio: "inherit" })`npx appium-installer`;
}
var cucumberTypes = {
paths: "array",
backtrace: "boolean",
dryRun: "boolean",
forceExit: "boolean",
failFast: "boolean",
format: "array",
formatOptions: "object",
import: "array",
language: "string",
name: "array",
order: "string",
publish: "boolean",
requireModule: "array",
retry: "number",
retryTagFilter: "string",
strict: "boolean",
tags: "string",
worldParameters: "object",
timeout: "number",
scenarioLevelReporter: "boolean",
tagsInTitle: "boolean",
ignoreUndefinedDefinitions: "boolean",
failAmbiguousDefinitions: "boolean",
tagExpression: "string",
profiles: "array",
file: "string"
};
var mochaTypes = {
require: "array",
compilers: "array",
allowUncaught: "boolean",
asyncOnly: "boolean",
bail: "boolean",
checkLeaks: "boolean",
delay: "boolean",
fgrep: "string",
forbidOnly: "boolean",
forbidPending: "boolean",
fullTrace: "boolean",
global: "array",
grep: "string",
invert: "boolean",
retries: "number",
timeout: "number",
ui: "string"
};
var jasmineTypes = {
defaultTimeoutInterval: "number",
helpers: "array",
requires: "array",
random: "boolean",
seed: "string",
failFast: "boolean",
failSpecWithNoExpectations: "boolean",
oneFailurePerSpec: "boolean",
grep: "string",
invertGrep: "boolean",
cleanStack: "boolean",
stopOnSpecFailure: "boolean",
stopSpecOnExpectationFailure: "boolean",
requireModule: "array"
};
function coerceOpts(types, opts) {
for (const key in opts) {
if (types[key] === "boolean" && typeof opts[key] === "string") {
opts[key] = opts[key] === "true";
} else if (types[key] === "number") {
opts[key] = Number(opts[key]);
} else if (types[key] === "array") {
opts[key] = Array.isArray(opts[key]) ? opts[key] : [opts[key]];
} else if (types[key] === "object" && typeof opts[key] === "string") {
opts[key] = JSON.parse(opts[key]);
}
}
return opts;
}
function coerceOptsFor(framework) {
if (framework === "cucumber") {
return coerceOpts.bind(null, cucumberTypes);
} else if (framework === "mocha") {
return coerceOpts.bind(null, mochaTypes);
} else if (framework === "jasmine") {
return coerceOpts.bind(null, jasmineTypes);
}
throw new Error(`Unsupported framework "${framework}"`);
}
var NodeVersion = /* @__PURE__ */ ((NodeVersion2) => {
NodeVersion2[NodeVersion2["major"] = 0] = "major";
NodeVersion2[NodeVersion2["minor"] = 1] = "minor";
NodeVersion2[NodeVersion2["patch"] = 2] = "patch";
return NodeVersion2;
})(NodeVersion || {});
function nodeVersion(type) {
return process.versions.node.split(".").map(Number)[NodeVersion[type]];
}
// src/interface.ts
var log2 = logger2("@wdio/cli");
var EVENT_FILTER = ["sessionStarted", "sessionEnded", "finishedCommand", "ready", "workerResponse", "workerEvent"];
var WDIOCLInterface = class extends EventEmitter {
constructor(_config, totalWorkerCnt, _isWatchMode = false) {
super();
this._config = _config;
this.totalWorkerCnt = totalWorkerCnt;
this._isWatchMode = _isWatchMode;
this.hasAnsiSupport = supportsColor && supportsColor.hasBasic;
this.totalWorkerCnt = totalWorkerCnt;
this._isWatchMode = _isWatchMode;
this._specFileRetries = _config.specFileRetries || 0;
this._specFileRetriesDelay = _config.specFileRetriesDelay || 0;
this.on("job:start", this.addJob.bind(this));
this.on("job:end", this.clearJob.bind(this));
this.setup();
this.onStart();
}
#snapshotManager = new SnapshotManager({
updateSnapshot: "new"
// ignored in this context
});
hasAnsiSupport;
result = {
finished: 0,
passed: 0,
retries: 0,
failed: 0
};
_jobs = /* @__PURE__ */ new Map();
_specFileRetries;
_specFileRetriesDelay;
_skippedSpecs = 0;
_inDebugMode = false;
_start = /* @__PURE__ */ new Date();
_messages = {
reporter: {},
debugger: {}
};
#hasShard() {
return this._config.shard && this._config.shard.total !== 1;
}
setup() {
this._jobs = /* @__PURE__ */ new Map();
this._start = /* @__PURE__ */ new Date();
this.result = {
finished: 0,
passed: 0,
retries: 0,
failed: 0
};
this._messages = {
reporter: {},
debugger: {}
};
}
onStart() {
const shardNote = this.#hasShard() ? ` (Shard ${this._config.shard.current} of ${this._config.shard.total})` : "";
this.log(chalk2.bold(`
Execution of ${chalk2.blue(this.totalWorkerCnt)} workers${shardNote} started at`), this._start.toISOString());
if (this._inDebugMode) {
this.log(chalk2.bgYellow(chalk2.black("DEBUG mode enabled!")));
}
if (this._isWatchMode) {
this.log(chalk2.bgYellow(chalk2.black("WATCH mode enabled!")));
}
this.log("");
}
onSpecRunning(rid) {
this.onJobComplete(rid, this._jobs.get(rid), 0, chalk2.bold(chalk2.cyan("RUNNING")));
}
onSpecRetry(rid, job, retries = 0) {
const delayMsg = this._specFileRetriesDelay > 0 ? ` after ${this._specFileRetriesDelay}s` : "";
this.onJobComplete(rid, job, retries, chalk2.bold(chalk2.yellow("RETRYING") + delayMsg));
}
onSpecPass(rid, job, retries = 0) {
this.onJobComplete(rid, job, retries, chalk2.bold(chalk2.green("PASSED")));
}
onSpecFailure(rid, job, retries = 0) {
this.onJobComplete(rid, job, retries, chalk2.bold(chalk2.red("FAILED")));
}
onSpecSkip(rid, job) {
this.onJobComplete(rid, job, 0, "SKIPPED", log2.info);
}
onJobComplete(cid, job, retries = 0, message = "", _logger = this.log) {
const details = [`[${cid}]`, message];
if (job) {
details.push("in", getRunnerName(job.caps), this.getFilenames(job.specs));
}
if (retries > 0) {
details.push(`(${retries} retries)`);
}
return _logger(...details);
}
onTestError(payload) {
const error = {
type: payload.error?.type || "Error",
message: payload.error?.message || (typeof payload.error === "string" ? payload.error : "Unknown error."),
stack: payload.error?.stack
};
return this.log(`[${payload.cid}]`, `${chalk2.red(error.type)} in "${payload.fullTitle}"
${chalk2.red(error.stack || error.message)}`);
}
getFilenames(specs = []) {
if (specs.length > 0) {
return "- " + specs.join(", ").replace(new RegExp(`${process.cwd()}`, "g"), "");
}
return "";
}
/**
* add job to interface
*/
addJob({ cid, caps, specs, hasTests }) {
this._jobs.set(cid, { caps, specs, hasTests });
if (hasTests) {
this.onSpecRunning(cid);
} else {
this._skippedSpecs++;
}
}
/**
* clear job from interface
*/
clearJob({ cid, passed, retries }) {
const job = this._jobs.get(cid);
this._jobs.delete(cid);
const retryAttempts = this._specFileRetries - retries;
const retry = !passed && retries > 0;
if (!retry) {
this.result.finished++;
}
if (job && job.hasTests === false) {
return this.onSpecSkip(cid, job);
}
if (passed) {
this.result.passed++;
this.onSpecPass(cid, job, retryAttempts);
} else if (retry) {
this.totalWorkerCnt++;
this.result.retries++;
this.onSpecRetry(cid, job, retryAttempts);
} else {
this.result.failed++;
this.onSpecFailure(cid, job, retryAttempts);
}
}
/**
* for testing purposes call console log in a static method
*/
log(...args) {
console.log(...args);
return args;
}
logHookError(error) {
if (error instanceof HookError) {
return this.log(`${chalk2.red(error.name)} in "${error.origin}"
${chalk2.red(error.stack || error.message)}`);
}
return this.log(`${chalk2.red(error.name)}: ${chalk2.red(error.stack || error.message)}`);
}
/**
* event handler that is triggered when runner sends up events
*/
onMessage(event) {
if (event.name === "reporterRealTime") {
this.log(event.content);
return;
}
if (event.origin === "debugger" && event.name === "start") {
this.log(chalk2.yellow(event.params.introMessage));
this._inDebugMode = true;
return this._inDebugMode;
}
if (event.origin === "debugger" && event.name === "stop") {
this._inDebugMode = false;
return this._inDebugMode;
}
if (event.name === "testFrameworkInit") {
return this.emit("job:start", event.content);
}
if (event.name === "snapshot") {
const snapshotResults = event.content;
return snapshotResults.forEach((snapshotResult) => {
this.#snapshotManager.add(snapshotResult);
});
}
if (event.name === "error") {
return this.log(
`[${event.cid}]`,
chalk2.white(chalk2.bgRed(chalk2.bold(" Error: "))),
event.content ? event.content.message || event.content.stack || event.content : ""
);
}
if (event.origin !== "reporter" && event.origin !== "debugger") {
if (EVENT_FILTER.includes(event.name)) {
return;
}
return this.log(event.cid, event.origin, event.name, event.content);
}
if (event.name === "printFailureMessage") {
return this.onTestError(event.content);
}
if (!this._messages[event.origin][event.name]) {
this._messages[event.origin][event.name] = [];
}
this._messages[event.origin][event.name].push(event.content);
}
sigintTrigger() {
if (this._inDebugMode) {
return false;
}
const isRunning = this._jobs.size !== 0 || this._isWatchMode;
const shutdownMessage = isRunning ? "Ending WebDriver sessions gracefully ...\n(press ctrl+c again to hard kill the runner)" : "Ended WebDriver sessions gracefully after a SIGINT signal was received!";
return this.log("\n\n" + shutdownMessage);
}
printReporters() {
const reporter = this._messages.reporter;
this._messages.reporter = {};
for (const [reporterName, messages] of Object.entries(reporter)) {
this.log("\n", chalk2.bold(chalk2.magenta(`"${reporterName}" Reporter:`)));
this.log(messages.join(""));
}
}
printSummary() {
const totalJobs = this.totalWorkerCnt - this.result.retries;
const elapsed = new Date(Date.now() - this._start.getTime()).toUTCString().match(/(\d\d:\d\d:\d\d)/)[0];
const retries = this.result.retries ? chalk2.yellow(this.result.retries, "retries") + ", " : "";
const failed = this.result.failed ? chalk2.red(this.result.failed, "failed") + ", " : "";
const skipped = this._skippedSpecs > 0 ? chalk2.gray(this._skippedSpecs, "skipped") + ", " : "";
const percentCompleted = totalJobs ? Math.round(this.result.finished / totalJobs * 100) : 0;
const snapshotSummary = this.#snapshotManager.summary;
const snapshotNotes = [];
if (snapshotSummary.added > 0) {
snapshotNotes.push(chalk2.green(`${snapshotSummary.added} snapshot(s) added.`));
}
if (snapshotSummary.updated > 0) {
snapshotNotes.push(chalk2.yellow(`${snapshotSummary.updated} snapshot(s) updated.`));
}
if (snapshotSummary.unmatched > 0) {
snapshotNotes.push(chalk2.red(`${snapshotSummary.unmatched} snapshot(s) unmatched.`));
}
if (snapshotSummary.unchecked > 0) {
snapshotNotes.push(chalk2.gray(`${snapshotSummary.unchecked} snapshot(s) unchecked.`));
}
if (snapshotNotes.length > 0) {
this.log("\nSnapshot Summary:");
snapshotNotes.forEach((note) => this.log(note));
}
return this.log(
"\nSpec Files: ",
chalk2.green(this.result.passed, "passed") + ", " + retries + failed + skipped + totalJobs,
"total",
`(${percentCompleted}% completed)`,
"in",
elapsed,
this.#hasShard() ? `
Shard: ${this._config.shard.current} / ${this._config.shard.total}` : "",
"\n"
);
}
finalise() {
this.printReporters();
this.printSummary();
}
};
// src/launcher.ts
var log3 = logger3("@wdio/cli:launcher");
var TS_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
var Launcher = class {
constructor(_configFilePath, _args = {}, _isWatchMode = false) {
this._configFilePath = _configFilePath;
this._args = _args;
this._isWatchMode = _isWatchMode;
this.configParser = new ConfigParser2(this._configFilePath, this._args);
}
#isInitialized = false;
configParser;
isMultiremote = false;
isParallelMultiremote = false;
runner;
interface;
_exitCode = 0;
_hasTriggeredExitRoutine = false;
_schedule = [];
_rid = [];
_runnerStarted = 0;
_runnerFailed = 0;
_launcher;
_resolve;
/**
* run sequence
* @return {Promise} that only gets resolved with either an exitCode or an error
*/
async run() {
await this.initialize();
const config = this.configParser.getConfig();
const capabilities = this.configParser.getCapabilities();
this.isParallelMultiremote = Array.isArray(capabilities) && capabilities.every((cap) => Object.values(cap).length > 0 && Object.values(cap).every((c) => typeof c === "object" && c.capabilities));
this.isMultiremote = this.isParallelMultiremote || !Array.isArray(capabilities);
validateConfig(TESTRUNNER_DEFAULTS, { ...config, capabilities });
await enableFileLogging(config.outputDir);
logger3.setLogLevelsConfig(config.logLevels, config.logLevel);
const totalWorkerCnt = Array.isArray(capabilities) ? capabilities.map((c) => {
if (this.isParallelMultiremote) {
const keys = Object.keys(c);
const caps2 = c[keys[0]].capabilities;
return this.configParser.getSpecs(caps2["wdio:specs"], caps2["wdio:exclude"]).length;
}
const standaloneCaps = c;
const cap = "alwaysMatch" in standaloneCaps ? standaloneCaps.alwaysMatch : standaloneCaps;
return this.configParser.getSpecs(cap["wdio:specs"], cap["wdio:exclude"]).length;
}).reduce((a, b) => a + b, 0) : 1;
this.interface = new WDIOCLInterface(config, totalWorkerCnt, this._isWatchMode);
config.runnerEnv.FORCE_COLOR = Number(this.interface.hasAnsiSupport).toString();
const [runnerName, runnerOptions] = Array.isArray(config.runner) ? config.runner : [config.runner, {}];
const Runner = (await initializePlugin(runnerName, "runner")).default;
this.runner = new Runner(runnerOptions, config);
exitHook(this._exitHandler.bind(this));
let exitCode = 0;
let error = void 0;
const caps = this.configParser.getCapabilities();
try {
const { ignoredWorkerServices, launcherServices } = await initializeLauncherService(config, caps);
this._launcher = launcherServices;
this._args.ignoredWorkerServices = ignoredWorkerServices;
await this.runner.initialize();
log3.info("Run onPrepare hook");
await runLauncherHook(config.onPrepare, config, caps);
await runServiceHook(this._launcher, "onPrepare", config, caps);
await Promise.all([
setupDriver(config, caps),
setupBrowser(config, caps)
]);
exitCode = await this._runMode(config, caps);
await logger3.waitForBuffer();
this.interface.finalise();
} catch (err) {
error = err;
} finally {
if (!this._hasTriggeredExitRoutine) {
this._hasTriggeredExitRoutine = true;
const passesCodeCoverage = await this.runner.shutdown();
if (!passesCodeCoverage) {
exitCode = exitCode || 1;
}
}
exitCode = await this.#runOnCompleteHook(config, caps, exitCode);
}
if (error) {
this.interface.logHookError(error);
throw error;
}
return exitCode;
}
/**
* initialize launcher by loading `tsx` if needed
*/
async initialize() {
if (this.#isInitialized) {
return;
}
const tsxPath = resolve2("tsx", import.meta.url);
if (!process.env.NODE_OPTIONS || !process.env.NODE_OPTIONS.includes(tsxPath)) {
const moduleLoaderFlag = nodeVersion("major") >= 21 || nodeVersion("major") === 20 && nodeVersion("minor") >= 6 || nodeVersion("major") === 18 && nodeVersion("minor") >= 19 ? "--import" : "--loader";
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ""} ${moduleLoaderFlag} ${tsxPath}`;
}
if (TS_FILE_EXTENSIONS.some((ext) => this._configFilePath.endsWith(ext))) {
await import(tsxPath);
}
this.#isInitialized = true;
await this.configParser.initialize(this._args);
}
/**
* run onComplete hook
* Even if it fails we still want to see result and end logger stream.
* Also ensure that user hooks are run before service hooks so that e.g.
* a user can use plugin service, e.g. shared store service is still
* available running hooks in this order
*/
async #runOnCompleteHook(config, caps, exitCode) {
log3.info("Run onComplete hook");
const onCompleteResults = await runOnCompleteHook(config.onComplete, config, caps, exitCode, this.interface.result);
if (this._launcher) {
await runServiceHook(this._launcher, "onComplete", exitCode, config, caps);
}
return onCompleteResults.includes(1) ? 1 : exitCode;
}
/**
* run without triggering onPrepare/onComplete hooks
*/
_runMode(config, caps) {
if (
/**
* no caps were provided
*/
!caps || /**
* capability array is empty
*/
Array.isArray(caps) && caps.length === 0 || /**
* user wants to use multiremote but capability object is empty
*/
!Array.isArray(caps) && Object.keys(caps).length === 0
) {
return new Promise((resolve3) => {
log3.error("Missing capabilities, exiting with failure");
return resolve3(1);
});
}
const specFileRetries = this._isWatchMode ? 0 : config.specFileRetries;
let cid = 0;
if (this.isMultiremote && !this.isParallelMultiremote) {
this._schedule.push({
cid: cid++,
caps,
specs: this._formatSpecs(caps, specFileRetries),
availableInstances: config.maxInstances || 1,
runningInstances: 0
});
} else {
for (const capabilities of caps) {
const availableInstances = this.isParallelMultiremote ? config.maxInstances || 1 : config.runner === "browser" ? 1 : capabilities["wdio:maxInstances"] || config.maxInstancesPerCapability;
this._schedule.push({
cid: cid++,
caps: capabilities,
specs: this._formatSpecs(capabilities, specFileRetries),
availableInstances,
runningInstances: 0
});
}
}
return new Promise((resolve3) => {
this._resolve = resolve3;
if (Object.values(this._schedule).reduce((specCnt, schedule) => specCnt + schedule.specs.length, 0) === 0) {
const { total, current } = config.shard;
if (total > 1) {
log3.info(`No specs to execute in shard ${current}/${total}, exiting!`);
return resolve3(0);
}
log3.error("No specs found to run, exiting with failure");
return resolve3(1);
}
if (this._runSpecs()) {
resolve3(0);
}
});
}
/**
* Format the specs into an array of objects with files and retries
*/
_formatSpecs(capabilities, specFileRetries) {
let caps;
if ("alwaysMatch" in capabilities) {
caps = capabilities.alwaysMatch;
} else if (typeof Object.keys(capabilities)[0] === "object" && "capabilities" in capabilities[Object.keys(capabilities)[0]]) {
caps = {};
} else {
caps = capabilities;
}
const specs = (
// @ts-expect-error deprecated
caps.specs || caps["wdio:specs"]
);
const excludes = (
// @ts-expect-error deprecated
caps.exclude || caps["wdio:exclude"]
);
const files = this.configParser.getSpecs(specs, excludes);
return files.map((file) => {
if (typeof file === "string") {
return { files: [file], retries: specFileRetries };
} else if (Array.isArray(file)) {
return { files: file, retries: specFileRetries };
}
log3.warn("Unexpected entry in specs that is neither string nor array: ", file);
return { files: [], retries: specFileRetries };
});
}
/**
* run multiple single remote tests
* @return {Boolean} true if all specs have been run and all instances have finished
*/
_runSpecs() {
if (this._hasTriggeredExitRoutine) {
return true;
}
const config = this.configParser.getConfig();
while (this._getNumberOfRunningInstances() < config.maxInstances) {
const schedulableCaps = this._schedule.filter((session) => {
const filter = typeof config.bail !== "number" || config.bail < 1 || config.bail > this._runnerFailed;
if (!filter) {
this._schedule.forEach((t) => {
t.specs = [];
});
return false;
}
if (this._getNumberOfRunningInstances() >= config.maxInstances) {
return false;
}
return session.availableInstances > 0 && session.specs.length > 0;
}).sort((a, b) => a.runningInstances - b.runningInstances);
if (schedulableCaps.length === 0) {
break;
}
const specs = schedulableCaps[0].specs.shift();
this._startInstance(
specs.files,
schedulableCaps[0].caps,
schedulableCaps[0].cid,
specs.rid,
specs.retries
);
schedulableCaps[0].availableInstances--;
schedulableCaps[0].runningInstances++;
}
return this._getNumberOfRunningInstances() === 0 && this._getNumberOfSpecsLeft() === 0;
}
/**
* gets number of all running instances
* @return {number} number of running instances
*/
_getNumberOfRunningInstances() {
return this._schedule.map((a) => a.runningInstances).reduce((a, b) => a + b);
}
/**
* get number of total specs left to complete whole suites
* @return {number} specs left to complete suite
*/
_getNumberOfSpecsLeft() {
return this._schedule.map((a) => a.specs.length).reduce((a, b) => a + b);
}
/**
* Start instance in a child process.
* @param {Array} specs Specs to run
* @param {number} cid Capabilities ID
* @param {string} rid Runner ID override
* @param {number} retries Number of retries remaining
*/
async _startInstance(specs, caps, cid, rid, retries) {
if (!this.runner || !this.interface) {
throw new Error("Internal Error: no runner initialized, call run() first");
}
const config = this.configParser.getConfig();
if (typeof config.specFileRetriesDelay === "number" && config.specFileRetries > 0 && config.specFileRetries !== retries) {
await sleep(config.specFileRetriesDelay * 1e3);
}
const runnerId = rid || this._getRunnerId(cid);
const processNumber = this._runnerStarted + 1;
const debugArgs = [];
let debugType;
let debugHost = "";
const debugPort = process.debugPort;
for (const arg of process.execArgv) {
const debugArgs2 = arg.match("--(debug|inspect)(?:-brk)?(?:=(.*):)?");
if (debugArgs2) {
const [, type, host] = debugArgs2;
if (type) {
debugType = type;
}
if (host) {
debugHost = `${host}:`;
}
}
}
if (debugType) {
debugArgs.push(`--${debugType}=${debugHost}${debugPort + processNumber}`);
}
const capExecArgs = [...config.execArgv || []];
const defaultArgs = capExecArgs.length ? process.execArgv : [];
const execArgv = [...defaultArgs, ...debugArgs, ...capExecArgs];
this._runnerStarted++;
const workerCaps = structuredClone(caps);
log3.info("Run onWorkerStart hook");
await runLauncherHook(config.onWorkerStart, runnerId, workerCaps, specs, this._args, execArgv).catch((error) => this._workerHookError(error));
await runServiceHook(this._launcher, "onWorkerStart", runnerId, workerCaps, specs, this._args, execArgv).catch((error) => this._workerHookError(error));
const worker = await this.runner.run({
cid: runnerId,
command: "run",
configFile: this._configFilePath,
args: {
...this._args,
/**
* Pass on user and key values to ensure they are available in the worker process when using
* environment variables that were locally exported but not part of the environment.
*/
user: config.user,
key: config.key
},
caps: workerCaps,
specs,
execArgv,
retries
});
worker.on("message", this.interface.onMessage.bind(this.interface));
worker.on("error", this.interface.onMessage.bind(this.interface));
worker.on("exit", (code) => {
if (!this.configParser.getConfig().groupLogsByTestSpec) {
return;
}
if (code.exitCode === 0) {
console.log(WORKER_GROUPLOGS_MESSAGES.normalExit(code.cid));
} else {
console.log(WORKER_GROUPLOGS_MESSAGES.exitWithError(code.cid));
}
worker.logsAggregator.forEach((logLine) => {
console.log(logLine.replace(new RegExp("\\n$"), ""));
});
});
worker.on("exit", this._endHandler.bind(this));
}
_workerHookError(error) {
if (!this.interface) {
throw new Error("Internal Error: no interface initialized, call run() first");
}
this.interface.logHookError(error);
if (this._resolve) {
this._resolve(1);
}
}
/**
* generates a runner id
* @param {number} cid capability id (unique identifier for a capability)
* @return {String} runner id (combination of cid and test id e.g. 0a, 0b, 1a, 1b ...)
*/
_getRunnerId(cid) {
if (!this._rid[cid]) {
this._rid[cid] = 0;
}
return `${cid}-${this._rid[cid]++}`;
}
/**
* Close test runner process once all child processes have exited
* @param {number} cid Capabilities ID
* @param {number} exitCode exit code of child process
* @param {Array} specs Specs that were run
* @param {number} retries Number or retries remaining
*/
async _endHandler({ cid: rid, exitCode, specs, retries }) {
const passed = this._isWatchModeHalted() || exitCode === 0;
if (!passed && retries > 0) {
const requeue = this.configParser.getConfig().specFileRetriesDeferred ? "push" : "unshift";
this._schedule[parseInt(rid, 10)].specs[requeue]({ files: specs, retries: retries - 1, rid });
} else {
this._exitCode = this._isWatchModeHalted() ? 0 : this._exitCode || exitCode;
this._runnerFailed += !passed ? 1 : 0;
}
if (!this._isWatchModeHalted() && this.interface) {
this.interface.emit("job:end", { cid: rid, passed, retries });
}
const cid = parseInt(rid, 10);
this._schedule[cid].availableInstances++;
this._schedule[cid].runningInstances--;
log3.info("Run onWorkerEnd hook");
const config = this.configParser.getConfig();
await runLauncherHook(config.onWorkerEnd, rid, exitCode, specs, retries).catch((error) => this._workerHookError(error));
await runServiceHook(this._launcher, "onWorkerEnd", rid, exitCode, specs, retries).catch((error) => this._workerHookError(error));
const shouldRunSpecs = this._runSpecs();
const inWatchMode = this._isWatchMode && !this._hasTriggeredExitRoutine;
if (!shouldRunSpecs || inWatchMode) {
if (inWatchMode) {
this.interface?.finalise();
}
return;
}
if (this._resolve) {
this._resolve(passed ? this._exitCode : 1);
}
}
/**
* We need exitHandler to catch SIGINT / SIGTERM events.
* Make sure all started selenium sessions get closed properly and prevent
* having dead driver processes. To do so let the runner end its Selenium
* session first before killing
*/
_exitHandler(callback) {
if (!callback || !this.runner || !this.interface) {
return;
}
if (this._hasTriggeredExitRoutine) {
return callback(true);
}
this._hasTriggeredExitRoutine = true;
this.interface.sigintTrigger();
return this.runner.shutdown().then(callback);
}
/**
* returns true if user stopped watch mode, ex with ctrl+c
* @returns {boolean}
*/
_isWatchModeHalted() {
return this._isWatchMode && this._hasTriggeredExitRoutine;
}
};
var launcher_default = Launcher;
// src/run.ts
import path7 from "node:path";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
// src/commands/config.ts
var config_exports = {};
__export(config_exports, {
builder: () => builder,
canAccessConfigPath: () => canAccessConfigPath,
cmdArgs: () => cmdArgs,
command: () => command,
desc: () => desc,
formatConfigFilePaths: () => formatConfigFilePaths,
handler: () => handler,
missingConfigurationPrompt: () => missingConfigurationPrompt,
parseAnswers: () => parseAnswers,
runConfigCommand: () => runConfigCommand
});
import fs3 from "node:fs/promises";
import fss from "node:fs";
import path3 from "node:path";
import inquirer2 from "inquirer";
var hasYarnLock = false;
try {
fss.accessSync("yarn.lock");
hasYarnLock = true;
} catch {
hasYarnLock = false;
}
var command = "config";
var desc = "Initialize WebdriverIO and setup configuration in your current project.";
var cmdArgs = {
yarn: {
type: "boolean",
desc: "Install packages via Yarn package manager.",
default: hasYarnLock
},
yes: {
alias: "y",
desc: "will fill in all config defaults without prompting",
type: "boolean",
default: false
},
npmTag: {
alias: "t",
desc: "define NPM tag to use for WebdriverIO related packages",
type: "string",
default: "latest"
}
};
var builder = (yargs2) => {
return yargs2.options(cmdArgs).epilogue(CLI_EPILOGUE).help();
};
var parseAnswers = async function(yes) {
console.log(CONFIG_HELPER_INTRO);
const answers = await getAnswers(yes);
const frameworkPackage = convertPackageHashToObject(answers.framework);
const runnerPackage = convertPackageHashToObject(answers.runner || SUPPORTED_PACKAGES.runner[0].value);
const servicePackages = answers.services.map((service) => convertPackageHashToObject(service));
const pluginPackages = answers.plugins.map((plugin) => convertPackageHashToObject(plugin));
const serenityPackages = getSerenityPackages(answers);
const reporterPackages = answers.reporters.map((reporter) => convertPackageHashToObject(reporter));
const presetPackage = convertPackageHashToObject(answers.preset || "");
const projectProps = await getProjectProps(process.cwd());
const projectRootDir = await getProjectRoot(answers);
const packagesToInstall = [
runnerPackage.package,
frameworkPackage.package,
presetPackage.package,
...reporterPackages.map((reporter) => reporter.package),
...pluginPackages.map((plugin) => plugin.package),
...servicePackages.map((service) => service.package),
...serenityPackages
].filter(Boolean);
const hasRootTSConfig = await fs3.access(path3.resolve(projectRootDir, "tsconfig.json")).then(() => true, () => false);
const tsConfigFilePath = !hasRootTSConfig ? path3.resolve(projectRootDir, "tsconfig.json") : answers.specs ? path3.resolve(
path3.dirname(answers.specs.split(path3.sep).filter((s) => !s.includes("*")).join(path3.sep)),
"tsconfig.json"
) : path3.resolve(projectRootDir, `tsconfig.${runnerPackage.short === "local" ? "e2e" : "wdio"}.json`);
const parsedPaths = getPathForFileGeneration(answers, projectRootDir);
const isUsingTypeScript = answers.isUsingTypeScript;
const wdioConfigFilename = `wdio.conf.${isUsingTypeScript ? "ts" : "js"}`;
const wdioConfigPath = path3.resolve(projectRootDir, wdioConfigFilename);
return {
projectName: projectProps?.packageJson.name || "Test Suite",
// default values required in templates
...{
usePageObjects: false,
installTestingLibrary: false
},
...answers,
useSauceConnect: isNuxtProject || answers.useSauceConnect,
rawAnswers: answers,
runner: runnerPackage.short,
preset: presetPackage.short,
framework: frameworkPackage.short,
purpose: runnerPackage.purpose,
serenityAdapter: frameworkPackage.package === "@serenity-js/webdriverio" && frameworkPackage.purpose,
reporters: reporterPackages.map(({ short }) => short),
plugins: pluginPackages.map(({ short }) => short),
services: servicePackages.map(({ short }) => short),
specs: answers.specs && `./${path3.relative(projectRootDir, answers.specs).replaceAll(path3.sep, "/")}`,
stepDefinitions: answers.stepDefinitions && `./${path3.relative(projectRootDir, answers.stepDefinitions).replaceAll(path3.sep, "/")}`,
packagesToInstall,
isUsingTypeScript,
esmSupport: projectProps && !projectProps.esmSupported ? false : true,
isSync: false,
_async: "async ",
_await: "await ",
projectRootDir,
destSpecRootPath: parsedPaths.destSpecRootPath,
destStepRootPath: parsedPaths.destStepRootPath,
destPageObjectRootPath: parsedPaths.destPageObjectRootPath,
destSerenityLibRootPath: parsedPaths.destSerenityLibRootPath,
relativePath: parsedPaths.relativePath,
hasRootTSConfig,
tsConfigFilePath,
tsProject: `./${path3.relative(projectRootDir, tsConfigFilePath).replaceAll(path3.sep, "/")}`,
wdioConfigPath
};
};
async function runConfigCommand(parsedAnswers, npmTag) {
console.log("\n");
await createPackageJSON(parsedAnswers);
await setupTypeScript(parsedAnswers);
await npmInstall(parsedAnswers, npmTag);
await createWDIOConfig(parsedAnswers);
await createWDIOScript(parsedAnswers);
console.log(
configHelperSuccessMessage({
projectRootDir: parsedAnswers.projectRootDir,
runScript: parsedAnswers.serenityAdapter ? "serenity" : "wdio",
extraInfo: parsedAnswers.serenityAdapter ? CONFIG_HELPER_SERENITY_BANNER : ""
})
);
await runAppiumInstaller(parsedAnswers);
}
async function handler(argv, runConfigCmd = runConfigCommand) {
const parsedAnswers = await parseAnswers(argv.yes);
await runConfigCmd(parsedAnswers, argv.npmTag);
return {
success: true,
parsedAnswers,
installedPackages: parsedAnswers.packagesToInstall.map((pkg2) => pkg2.split("--")[0])
};
}
async function formatConfigFilePaths(config) {
const fullPath = path3.isAbsolute(config) ? config : path3.join(process.cwd(), config);
const fullPathNoExtension = fullPath.substring(0, fullPath.lastIndexOf(path3.extname(fullPath)));
return { fullPath, fullPathNoExtension };
}
async function canAccessConfigPath(configPathNoExtension, configPath) {
return new Promise((resolve3, reject) => {
if (configPath) {
fs3.access(configPath).then(() => resolve3(configPath), reject);
} else {
reject();
}
}).catch(() => Promise.all(SUPPORTED_CONFIG_FILE_EXTENSION.map(async (supportedExtension) => {
const configPathWithExtension = `${configPathNoExtension}.${supportedExtension}`;
return fs3.access(configPathWithExtension).then(() => configPathWithExtension, () => void 0);
})).then(
(configFilePaths) => configFilePaths.find(Boolean),
() => void 0
));
}
async function missingConfigurationPrompt(command5, configPath, runConfigCmd = runConfigCommand) {
const message = `Could not execute "${command5}" due to missing configuration, file "${path3.parse(configPath).name}[.js/.ts]" not found! Would you like to create one?`;
const { config } = await inquirer2.prompt({
type: "confirm",
name: "config",
message,
default: false
});
if (!config) {
console.log(`No WebdriverIO configuration found in "${process.cwd()}"`);
if (!process.env.WDIO_UNIT_TESTS) {
process.exit(0);
}
return configPath;
}
const parsedAnswers = await parseAnswers(false);
await runConfigCmd(parsedAnswers, "latest");
return configPath;
}
// src/commands/install.ts
var install_exports = {};
__export(install_exports, {
builder: () => builder2,
cmdArgs: () => cmdArgs2,
command: () => command2,
desc: () => desc2,
handler: () => handler2
});
import fs4 from "node:fs/promises";
import path4 from "node:path";
var supportedInstallations = {
runner: SUPPORTED_PACKAGES.runner.map(({ value }) => convertPackageHashToObject(value)),
plugin: SUPPORTED_PACKAGES.plugin.map(({ value }) => convertPackageHashToObject(value)),
service: SUPPORTED_PACKAGES.service.map(({ value }) => convertPackageHashToObject(value)),
reporter: SUPPORTED_PACKAGES.reporter.map(({ value }) => convertPackageHashToObject(value)),
framework: SUPPORTED_PACKAGES.framework.map(({ value }) => convertPackageHashToObject(value))
};
var command2 = "install <type> <name>";
var desc2 = [
"Add a `reporter`, `service`, or `framework` to your WebdriverIO project.",
"The command installs the package from NPM, adds it to your package.json",
"and modifies the wdio.conf.js accordingly."
].join(" ");
var cmdArgs2 = {
config: {
desc: "Location of your WDIO configuration (default: wdio.conf.(js|ts|cjs|mjs))"
}
};
var builder2 = (yargs2) => {
yargs2.options(cmdArgs2).epilogue(CLI_EPILOGUE).help();
for (const [type, plugins] of Object.entries(supportedInstallations)) {
for (const plugin of plugins) {
yargs2.example(`$0 install ${type} ${plugin.short}`, `Install ${plugin.package}`);
}
}
return yargs2;
};
async function handler2(argv) {
const { type, name, config } = argv;
if (!Object.keys(supportedInstallations).includes(type)) {
console.log(`Type ${type} is not supported.`);
process.exit(0);
return;
}
const options = supportedInstallations[type].map((pkg2) => pkg2.short);
if (!options.find((pkg2) => pkg2 === name)) {
console.log(
`Error: ${name} is not a supported ${type}.
Available options for a ${type} are:
- ${options.join("\n- ")}`
);
process.exit(0);
return;
}
const defaultPath = path4.resolve(process.cwd(), "wdio.conf");
const wdioConfPathWithNoExtension = config ? (await formatConfigFilePaths(config)).fullPathNoExtension : defaultPath;
const wdioConfPath = await canAccessConfigPath(wdioConfPathWithNoExtension);
if (!wdioConfPath) {
try {
await missingConfigurationPrompt("install", wdioConfPathWithNoExtension);
return handler2(argv);
} catch {
process.exit(1);
return;
}
}
const configFile = await fs4.readFile(wdioConfPath, { encoding: "utf-8" });
const match = findInConfig(configFile, type);
const projectRoot = await getProjectRoot();
if (match && match[0].includes(name)) {
console.log(`The ${type} ${name} is already part of your configuration.`);
process.exit(0);
return;
}
const selectedPackage = supportedInstallations[type].find(({ short }) => short === name);
const pkgsToInstall = selectedPackage ? [selectedPackage.package] : [];
addServiceDeps(selectedPackage ? [selectedPackage] : [], pkgsToInstall, true);
const pm = detectPackageManager();
console.log(`Installing "${selectedPackage.package}" using ${pm}.`);
const success = await installPackages(projectRoot, pkgsToInstall, true);
if (!success) {
process.exit(1);
return;
}
console.log(`Package "${selectedPackage.package}" installed successfully.`);
const newConfig = replaceConfig(configFile, type, name);
if (!newConfig) {
throw new Error(`Couldn't find "${type}" property in ${path4.basename(wdioConfPath)}`);
}
await fs4.writeFile(wdioConfPath, newConfig, { encoding: "utf-8" });
console.log("Your wdio.conf.js file has been updated.");
process.exit(0);
}
// src/commands/repl.ts
var repl_exports = {};
__export(repl_exports, {
builder: () => builder4,
cmdArgs: () => cmdArgs4,
command: () => command4,
desc: () => desc4,
handler: () => handler4
});
import pickBy3 from "lodash.pickby";
import { remote } from "webdriverio";
// src/commands/run.ts
var run_exports = {};
__export(run_exports, {
builder: () => builder3,
cmdArgs: () => cmdArgs3,
command: () => command3,
desc: () => desc3,
handler: () => handler3,
launch: () => launch,
launchWithStdin: () => launchWithStdin
});
import fs5 from "node:fs/promises";
import path6 from "node:path";
// src/watcher.ts
import url from "node:url";
import path5 from "node:path";
import chokidar from "chokidar";
import pickBy2 from "lodash.pickby";
import flattenDeep from "lodash.flattendeep";
import union from "lodash.union";
import logger4 from "@wdio/logger";
import { FileSystemPathService } from "@wdio/config/node";
var log4 = logger4("@wdio/cli:watch");
var Watcher = class {
constructor(_configFile, _args) {
this._configFile = _configFile;
this._args = _args;
log4.info("Starting launcher in watch mode");
this._launcher = new launcher_default(this._configFile, this._args, true);
}
_launcher;
_specs = [];
async watch() {
await this._launcher.initialize();
const specs = this._launcher.configParser.getSpecs();
const capSpecs = this._launcher.isMultiremote ? [] : union(flattenDeep(
this._launcher.configParser.getCapabilities().map((cap) => "alwaysMatch" in cap ? cap.alwaysMatch["wdio:specs"] : cap["wdio:specs"] || [])
));
this._specs = [...specs, ...capSpecs];
const flattenedSpecs = flattenDeep(this._specs).map((fileUrl) => url.fileURLToPath(fileUrl));
chokidar.watch(flattenedSpecs, { ignoreInitial: true }).on("add", this.getFileListener()).on("change", this.getFileListener());
const { filesToWatch } = this._launcher.configParser.getConfig();
if (filesToWatch.length) {
const pathService = new FileSystemPathService();
const rootDir = path5.dirname(path5.resolve(process.cwd(), this._configFile));
const globbedFilesToWatch = filesToWatch.map((file) => pathService.ensureAbsolutePath(file, rootDir));
chokidar.watch(globbedFilesToWatch, { ignoreInitial: true }).on("add", this.getFileListener(false)).on("change", this.getFileListener(false));
}
await this._launcher.run();
const workers = this.getWorkers();
Object.values(workers).forEach((worker) => worker.on("exit", () => {
if (Object.values(workers).find((w) => w.isBusy)) {
return;
}
this._launcher.interface?.finalise();
}));
}
/**
* return file listener callback that calls `run` method
* @param {Boolean} [passOnFile=true] if true pass on file change as parameter
* @return {Function} chokidar event callback
*/
getFileListener(passOnFile = true) {
return (spec) => {
const runSpecs = [];
let singleSpecFound = false;
for (let index = 0, length = this._specs.length; index < length; index += 1) {
const value = this._specs[index];
if (Array.isArray(value) && value.indexOf(spec) > -1) {
runSpecs.push(value);
} else if (!singleSpecFound && spec === value) {
singleSpecFound = true;
runSpecs.push(value);
}
}
if (runSpecs.length === 0) {
runSpecs.push(url.pathToFileURL(spec).href);
}
const { spec: _, ...args } = this._args;
return runSpecs.map((spec2) => {
return this.run({
...args,
...passOnFile ? { spec: [spec2] } : {}
});
});
};
}
/**
* helper method to get workers from worker pool of wdio runner
* @param predicate filter by property value (see lodash.pickBy)
* @param includeBusyWorker don't filter out busy worker (default: false)
* @return Object with workers, e.g. {'0-0': { ... }}
*/
getWorkers(predicate, includeBusyWorker) {
if (!this._launcher.runner) {
throw new Error("Internal Error: no runner initialized, call run() first");
}
let workers = this._launcher.runner.workerPool;
if (typeof predicate === "function") {
workers = pickBy2(workers, predicate);
}
if (!includeBusyWorker) {
workers = pickBy2(workers, (worker) => !worker.isBusy);
}
return workers;
}
/**
* run workers with params
* @param params parameters to run the worker with
*/
run(params = {}) {
const workers = this.getWorkers(
params.spec ? (worker) => Boolean(worker.specs.find((s) => params.spec?.includes(s))) : void 0
);
if (Object.keys(workers).length === 0 || !this._launcher.interface) {
return;
}
this._launcher.interface.totalWorkerCnt = Object.entries(workers).length;
this.cleanUp();
for (const [, worker] of Object.entries(workers)) {
const { cid, capabilities, specs, sessionId } = worker;
const { hostname, path: path8, port, protocol, automationProtocol } = worker.config;
const args = Object.assign({ sessionId, baseUrl: worker.config.baseUrl, hostname, path: path8, port, protocol, automationProtocol }, params);
worker.postMessage("run", args);
this._launcher.interface.emit("job:start", { cid, caps: capabilities, specs });
}
}
cleanUp() {
this._launcher.interface?.setup();
}
};
// src/commands/run.ts
var command3 = "run <configPath>";
var desc3 = "Run your WDIO configuration file to initialize your tests. (default)";
var cmdArgs3 = {
watch: {
desc: "Run WebdriverIO in watch mode",
type: "boolean"
},
hostname: {
alias: "h",
desc: "automation driver host address",
type: "string"
},
port: {
alias: "p",
desc: "automation driver port",
type: "number"
},
path: {
type: "string",
desc: 'path to WebDriver endpoints (default "/")'
},
user: {
alias: "u",
desc: "username if using a cloud service as automation backend",
type: "string"
},
key: {
alias: "k",
desc: "corresponding access key to the user",
type: "string"
},
logLevel: {
alias: "l",
desc: "level of logging verbosity",
choices: ["trace", "debug", "info", "warn", "error", "silent"]
},
bail: {
desc: "stop test runner after specific amount of tests have failed",
type: "number"
},
baseUrl: {
desc: "shorten url command calls by setting a base url",
type: "string"
},
waitforTimeout: {
alias: "w",
desc: "timeout for all waitForXXX commands",
type: "number"
},
updateSnapshots: {
alias: "s",
desc: "update DOM, image or test snapshots",
type: "string",
coerce: (value) => {
if (value === "") {
return "all";
}
return value;
}
},
framework: {
alias: "f",
desc: "defines the framework (Mocha, Jasmine or Cucumber) to run the specs",
type: "string"
},
reporters: {
alias: "r",
desc: "reporters to print out the results on stdout",
type: "array"
},
suite: {
desc: "overwrites the specs attribute and runs the defined suite",
type: "array"
},
spec: {
desc: "run only a certain spec file or wildcard - overrides specs piped from stdin",
type: "array"
},
exclude: {
desc: "exclude certain spec file or wildcard from the test run - overrides exclude piped from stdin",
type: "array"
},
"repeat": {
desc: "Repeat specific specs and/or suites N times",
type: "number"
},
mochaOpts: {
desc: "Mocha options",
coerce: coerceOptsFor("mocha")
},
jasmineOpts: {
desc: "Jasmine options",
coerce: coerceOptsFor("jasmine")
},
cucumberOpts: {
desc: "Cucumber options",
coerce: coerceOptsFor("cucumber")
},
coverage: {
desc: "Enable coverage for browser runner"
},
shard: {
desc: "Shard tests and execute only the selected shard. Specify in the one-based form like `--shard x/y`, where x is the current and y the total shard.",
coerce: (shard) => {
const [current, total] = shard.split("/").map(Number);
if (Number.isNaN(current) || Number.isNaN(total)) {
throw new Error("Shard parameter must be in the form `x/y`, where x and y are positive integers.");
}
return { current, total };
}
}
};
var builder3 = (yargs2) => {
return yargs2.options(cmdArgs3).example("$0 run wdio.conf.js --suite foobar", 'Run suite on testsuite "foobar"').example("$0 run wdio.conf.js --spec ./tests/e2e/a.js --spec ./tests/e2e/b.js", "Run suite on specific specs").example("$0 run wdio.conf.js --shard 1/4", "Run only the first shard of 4 shards").example("$0 run wdio.conf.js --mochaOpts.timeout 60000", "Run suite with custom Mocha timeout").example("$0 run wdio.conf.js --tsConfigPath=./configs/bdd-tsconfig.json", "Run suite with tsx using custom tsconfig.json").epilogue(CLI_EPILOGUE).help();
};
function launchWithStdin(wdioConfPath, params) {
let stdinData = "";
process.stdin.resume();
const stdin = process.stdin;
stdin.setEncoding("utf8");
stdin.on("data", (data) => {
stdinData += data;
});
stdin.on("end", () => {
if (stdinData.length > 0) {
params.spec = stdinData.trim().split(/\r?\n/);
}
launch(wdioConfPath, params);
});
}
async function launch(wdioConfPath, params) {
const launcher = new launcher_default(wdioConfPath, params);
return launcher.run().then((...args) => {
if (!process.env.WDIO_UNIT_TESTS) {
process.exit(...args);
}
}).catch((err) => {
console.error(err);
if (!process.env.WDIO_UNIT_TESTS) {
process.exit(1);
}
});
}
async function handler3(argv) {
const { configPath = "wdio.conf.js", ...params } = argv;
const wdioConf = await formatConfigFilePaths(configPath);
const confAccess = await canAccessConfigPath(wdioConf.fullPathNoExtension, wdioConf.fullPath);
if (!confAccess) {
try {
await missingConfigurationPrompt("run", wdioConf.fullPathNoExtension);
if (process.env.WDIO_UNIT_TESTS) {
return;
}
return handler3(argv);
} catch {
process.exit(1);
}
}
const tsConfigPathFromEnvVar = process.env.TSCONFIG_PATH && path6.resolve(process.cwd(), process.env.TSCONFIG_PATH) || process.env.TSX_TSCONFIG_PATH && path6.resolve(process.cwd(), process.env.TSX_TSCONFIG_PATH);
const tsConfigPathFromParams = params.tsConfigPath && path6.resolve(process.cwd(), params.tsConfigPath);
const tsConfigPathRelativeToWdioConfig = path6.join(path6.dirname(confAccess), "tsconfig.json");
const localTSConfigPath = tsConfigPathFromEnvVar || tsConfigPathFromParams || tsConfigPathRelativeToWdioConfig;
const hasLocalTSConfig = await fs5.access(localTSConfigPath).then(() => true, () => false);
if (hasLocalTSConfig) {
process.env.TSX_TSCONFIG_PATH = localTSConfigPath;
}
if (params.watch) {
const watcher = new Watcher(confAccess, params);
return watcher.watch();
}
if (process.stdin.isTTY || !process.stdout.isTTY) {
return launch(confAccess, params);
}
launchWithStdin(confAccess, params);
}
// src/commands/repl.ts
var IGNORED_ARGS = [
"bail",
"framework",
"reporters",
"suite",
"spec",
"exclude",
"mochaOpts",
"jasmineOpts",
"cucumberOpts"
];
var command4 = "repl <option> [capabilities]";
var desc4 = "Run WebDriver session in command line";
var cmdArgs4 = {
platformVersion: {
alias: "v",
desc: "Version of OS for mobile devices",
type: "string"
},
deviceName: {
alias: "d",
desc: "Device name for mobile devices",
type: "string"
},
udid: {
alias: "u",
desc: "UDID of real mobile devices",
type: "string"
}
};
var builder4 = (yargs2) => {
return yargs2.options(pickBy3({ ...cmdArgs4, ...cmdArgs3 }, (_, key) => !IGNORED_ARGS.includes(key))).example("$0 repl firefox --path /", "Run repl locally").example("$0 repl chrome -u <SAUCE_USERNAME> -k <SAUCE_ACCESS_KEY>", "Run repl in Sauce Labs cloud").example("$0 repl android", "Run repl browser on launched Android device").example('$0 repl "./path/to/your_app.app"', "Run repl native app on iOS simulator").example('$0 repl ios -v 11.3 -d "iPhone 7" -u 123432abc', "Run repl browser on iOS device with capabilities").example('$0 repl "./path/to/wdio.config.js" 0 -p 9515', "Run repl using the first capability from the capabilty array in wdio.config.js").example('$0 repl "./path/to/wdio.config.js" "myChromeBrowser" -p 9515', "Run repl using a named multiremote capabilities in wdio.config.js").epilogue(CLI_EPILOGUE).help();
};
var handler4 = async (argv) => {
const caps = await getCapabilities(argv);
const client = await remote({ ...argv, ...caps });
global.$ = client.$.bind(client);
global.$$ = client.$$.bind(client);
global.browser = client;
await client.debug();
return client.deleteSession();
};
// src/commands/index.ts
var commands = [config_exports, install_exports, repl_exports, run_exports];
// src/run.ts
var DEFAULT_CONFIG_FILENAME = "wdio.conf.js";
var DESCRIPTION = [
"The `wdio` command allows you run and manage your WebdriverIO test suite.",
"If no command is provided it calls the `run` command by default, so:",
"",
"$ wdio wdio.conf.js",
"",
"is the same as:",
"$ wdio run wdio.conf.js",
"",
"For more information, visit: https://webdriver.io/docs/clioptions"
];
async function run() {
const argv = yargs(hideBin(process.argv)).command(commands).example("wdio run wdio.conf.js --suite foobar", 'Run suite on testsuite "foobar"').example("wdio run wdio.conf.js --spec ./tests/e2e/a.js --spec ./tests/e2e/b.js", "Run suite on specific specs").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5", "Run scenario by line number").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5:10", "Run scenarios by line number").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5:10 --spec ./test/e2e/b.feature", "Run scenarios by line number in single feature and another complete feature").example("wdio install reporter spec", "Install @wdio/spec-reporter").example("wdio repl chrome -u <SAUCE_USERNAME> -k <SAUCE_ACCESS_KEY>", "Run repl in Sauce Labs cloud").updateStrings({ "Commands:": `${DESCRIPTION.join("\n")}
Commands:` }).version(pkg.version).epilogue(CLI_EPILOGUE);
if (!process.argv.find((arg) => arg === "--help")) {
argv.options(cmdArgs3);
}
const params = await argv.parse();
if (!params._ || params._.find((param) => SUPPORTED_COMMANDS.includes(param))) {
return;
}
const args = {
...params,
configPath: path7.resolve(process.cwd(), params._[0] && params._[0].toString() || DEFAULT_CONFIG_FILENAME)
};
try {
const cp = await handler3(args);
return cp;
} catch (err) {
const output = await new Promise(
(resolve3) => yargs(hideBin(process.argv)).parse("--help", (err2, argv2, output2) => resolve3(output2))
);
console.error(`${output}
${err.stack}`);
if (!process.env.WDIO_UNIT_TESTS) {
process.exit(1);
}
}
}
export {
launcher_default as Launcher,
run
};