UNPKG

@wdio/cli

Version:
1,376 lines (1,363 loc) 121 kB
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); awa