@wdio/cli
Version:
WebdriverIO testrunner command line interface
1,376 lines (1,363 loc) • 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);
awa