UNPKG

@wdio/cli

Version:
1,708 lines 58.9 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 } from "import-meta-resolve";
import logger3 from "@wdio/logger";
import { validateConfig, DEFAULT_MAX_INSTANCES_PER_CAPABILITY_VALUE } 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 chalk, { supportsColor } from "chalk";
import logger2 from "@wdio/logger";
import { SnapshotManager } from "@vitest/snapshot/manager";

// src/utils.ts
import pickBy from "lodash.pickby";
import logger from "@wdio/logger";
import { SevereServiceError } from "webdriverio";
import { ConfigParser } from "@wdio/config/node";
import { CAPABILITY_KEYS } from "@wdio/protocols";

// src/constants.ts
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 SUPPORTED_COMMANDS = ["run", "install", "config", "repl"];
var ANDROID_CONFIG = {
  platformName: "Android",
  automationName: "UiAutomator2",
  deviceName: "Test"
};
var IOS_CONFIG = {
  platformName: "iOS",
  automationName: "XCUITest",
  deviceName: "iPhone Simulator"
};
var TS_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
var SUPPORTED_SNAPSHOTSTATE_OPTIONS = ["all", "new", "none"];
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/utils.ts
var log = logger("@wdio/cli:utils");
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, config3, capabilities, exitCode, results) {
  if (typeof onCompleteHook === "function") {
    onCompleteHook = [onCompleteHook];
  }
  return Promise.all(onCompleteHook.map(async (hook) => {
    try {
      await hook(exitCode, config3, 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;
}
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 (/\.(m|c)?(js|tsx?)$/.test(arg.option)) {
    if (TS_FILE_EXTENSIONS.some((ext) => arg.option.endsWith(ext))) {
      const { resolve: resolve2 } = await import("import-meta-resolve");
      const tsxPath = resolve2("tsx", import.meta.url);
      await import(tsxPath);
    }
    const config3 = new ConfigParser(arg.option);
    try {
      await config3.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 = config3.getCapabilities();
    requiredCaps = // multi capabilities
    requiredCaps[parseInt(arg.capabilities, 10)] || // multiremote
    requiredCaps[arg.capabilities]?.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 } };
}
var cucumberTypes = {
  paths: "array",
  backtrace: "boolean",
  dryRun: "boolean",
  forceExit: "boolean",
  failFast: "boolean",
  format: "array",
  formatOptions: "object",
  import: "array",
  language: "string",
  name: "array",
  order: "string",
  publish: "boolean",
  requireModule: "array",
  retry: "number",
  retryTagFilter: "string",
  strict: "boolean",
  tags: "string",
  worldParameters: "object",
  timeout: "number",
  scenarioLevelReporter: "boolean",
  tagsInTitle: "boolean",
  ignoreUndefinedDefinitions: "boolean",
  failAmbiguousDefinitions: "boolean",
  tagExpression: "string",
  profiles: "array",
  file: "string"
};
var mochaTypes = {
  require: "array",
  compilers: "array",
  allowUncaught: "boolean",
  asyncOnly: "boolean",
  bail: "boolean",
  checkLeaks: "boolean",
  delay: "boolean",
  fgrep: "string",
  forbidOnly: "boolean",
  forbidPending: "boolean",
  fullTrace: "boolean",
  global: "array",
  grep: "string",
  invert: "boolean",
  retries: "number",
  timeout: "number",
  ui: "string"
};
var jasmineTypes = {
  defaultTimeoutInterval: "number",
  helpers: "array",
  requires: "array",
  random: "boolean",
  seed: "string",
  failFast: "boolean",
  failSpecWithNoExpectations: "boolean",
  oneFailurePerSpec: "boolean",
  grep: "string",
  invertGrep: "boolean",
  cleanStack: "boolean",
  stopOnSpecFailure: "boolean",
  stopSpecOnExpectationFailure: "boolean",
  requireModule: "array"
};
function coerceOpts(types, opts) {
  for (const key in opts) {
    if (types[key] === "boolean" && typeof opts[key] === "string") {
      opts[key] = opts[key] === "true";
    } else if (types[key] === "number") {
      opts[key] = Number(opts[key]);
    } else if (types[key] === "array") {
      opts[key] = Array.isArray(opts[key]) ? opts[key] : [opts[key]];
    } else if (types[key] === "object" && typeof opts[key] === "string") {
      opts[key] = JSON.parse(opts[key]);
    }
  }
  return opts;
}
function coerceOptsFor(framework) {
  if (framework === "cucumber") {
    return coerceOpts.bind(null, cucumberTypes);
  } else if (framework === "mocha") {
    return coerceOpts.bind(null, mochaTypes);
  } else if (framework === "jasmine") {
    return coerceOpts.bind(null, jasmineTypes);
  }
  throw new Error(`Unsupported framework "${framework}"`);
}
var NodeVersion = /* @__PURE__ */ ((NodeVersion2) => {
  NodeVersion2[NodeVersion2["major"] = 0] = "major";
  NodeVersion2[NodeVersion2["minor"] = 1] = "minor";
  NodeVersion2[NodeVersion2["patch"] = 2] = "patch";
  return NodeVersion2;
})(NodeVersion || {});
function nodeVersion(type) {
  return process.versions.node.split(".").map(Number)[NodeVersion[type]];
}

// src/interface.ts
var log2 = logger2("@wdio/cli");
var EVENT_FILTER = ["sessionStarted", "sessionEnded", "finishedCommand", "ready", "workerResponse", "workerEvent"];
var WDIOCLInterface = class extends EventEmitter {
  constructor(_config, totalWorkerCnt, _isWatchMode = false) {
    super();
    this._config = _config;
    this.totalWorkerCnt = totalWorkerCnt;
    this._isWatchMode = _isWatchMode;
    this.hasAnsiSupport = supportsColor && supportsColor.hasBasic;
    this.totalWorkerCnt = totalWorkerCnt;
    this._isWatchMode = _isWatchMode;
    this._specFileRetries = _config.specFileRetries || 0;
    this._specFileRetriesDelay = _config.specFileRetriesDelay || 0;
    this.on("job:start", this.addJob.bind(this));
    this.on("job:end", this.clearJob.bind(this));
    this.setup();
    this.onStart();
  }
  _config;
  totalWorkerCnt;
  _isWatchMode;
  #snapshotManager = new SnapshotManager({
    updateSnapshot: "new"
    // ignored in this context
  });
  hasAnsiSupport;
  result = {
    finished: 0,
    passed: 0,
    retries: 0,
    failed: 0
  };
  _jobs = /* @__PURE__ */ new Map();
  _specFileRetries;
  _specFileRetriesDelay;
  _skippedSpecs = 0;
  _inDebugMode = false;
  _start = /* @__PURE__ */ new Date();
  _messages = {
    reporter: {},
    debugger: {}
  };
  #hasShard() {
    return this._config.shard && this._config.shard.total !== 1;
  }
  setup() {
    this._jobs = /* @__PURE__ */ new Map();
    this._start = /* @__PURE__ */ new Date();
    this.result = {
      finished: 0,
      passed: 0,
      retries: 0,
      failed: 0
    };
    this._messages = {
      reporter: {},
      debugger: {}
    };
  }
  onStart() {
    const shardNote = this.#hasShard() ? ` (Shard ${this._config.shard.current} of ${this._config.shard.total})` : "";
    this.log(chalk.bold(`
Execution of ${chalk.blue(this.totalWorkerCnt)} workers${shardNote} started at`), this._start.toISOString());
    if (this._inDebugMode) {
      this.log(chalk.bgYellow(chalk.black("DEBUG mode enabled!")));
    }
    if (this._isWatchMode) {
      this.log(chalk.bgYellow(chalk.black("WATCH mode enabled!")));
    }
    this.log("");
  }
  onSpecRunning(rid) {
    this.onJobComplete(rid, this._jobs.get(rid), 0, chalk.bold(chalk.cyan("RUNNING")));
  }
  onSpecRetry(rid, job, retries = 0) {
    const delayMsg = this._specFileRetriesDelay > 0 ? ` after ${this._specFileRetriesDelay}s` : "";
    this.onJobComplete(rid, job, retries, chalk.bold(chalk.yellow("RETRYING") + delayMsg));
  }
  onSpecPass(rid, job, retries = 0) {
    this.onJobComplete(rid, job, retries, chalk.bold(chalk.green("PASSED")));
  }
  onSpecFailure(rid, job, retries = 0) {
    this.onJobComplete(rid, job, retries, chalk.bold(chalk.red("FAILED")));
  }
  onSpecSkip(rid, job) {
    this.onJobComplete(rid, job, 0, "SKIPPED", log2.info);
  }
  onJobComplete(cid, job, retries = 0, message = "", _logger = this.log) {
    const details = [`[${cid}]`, message];
    if (job) {
      const caps = job.caps;
      const version = caps?.browserVersion || caps?.["appium:platformVersion"];
      const runnerName = getRunnerName(caps);
      if (version) {
        details.push("in", `${runnerName}(${version})`);
      } else {
        details.push("in", runnerName);
      }
      if (caps?.platformName || caps?.["appium:deviceName"]) {
        details.push("on", caps?.platformName || caps?.["appium:deviceName"]);
      }
      details.push(this.getFilenames(job.specs));
    }
    if (retries > 0) {
      details.push(`(${retries} retries)`);
    }
    return _logger(...details);
  }
  onTestError(payload) {
    const error = {
      type: payload.error?.type || "Error",
      message: payload.error?.message || (typeof payload.error === "string" ? payload.error : "Unknown error."),
      stack: payload.error?.stack
    };
    return this.log(`[${payload.cid}]`, `${chalk.red(error.type)} in "${payload.fullTitle}"
${chalk.red(error.stack || error.message)}`);
  }
  getFilenames(specs = []) {
    if (specs.length > 0) {
      return "- " + specs.join(", ").replace(new RegExp(`${process.cwd()}`, "g"), "");
    }
    return "";
  }
  /**
   * add job to interface
   */
  addJob({ cid, caps, specs, hasTests }) {
    this._jobs.set(cid, { caps, specs, hasTests });
    if (hasTests) {
      this.onSpecRunning(cid);
    } else {
      this._skippedSpecs++;
    }
  }
  /**
   * clear job from interface
   */
  clearJob({ cid, passed, retries }) {
    const job = this._jobs.get(cid);
    this._jobs.delete(cid);
    const retryAttempts = this._specFileRetries - retries;
    const retry = !passed && retries > 0;
    if (!retry) {
      this.result.finished++;
    }
    if (job && job.hasTests === false) {
      return this.onSpecSkip(cid, job);
    }
    if (passed) {
      this.result.passed++;
      this.onSpecPass(cid, job, retryAttempts);
    } else if (retry) {
      this.totalWorkerCnt++;
      this.result.retries++;
      this.onSpecRetry(cid, job, retryAttempts);
    } else {
      this.result.failed++;
      this.onSpecFailure(cid, job, retryAttempts);
    }
  }
  /**
   * for testing purposes call console log in a static method
   */
  log(...args) {
    console.log(...args);
    return args;
  }
  logHookError(error) {
    if (error instanceof HookError) {
      return this.log(`${chalk.red(error.name)} in "${error.origin}"
${chalk.red(error.stack || error.message)}`);
    }
    return this.log(`${chalk.red(error.name)}: ${chalk.red(error.stack || error.message)}`);
  }
  /**
   * event handler that is triggered when runner sends up events
   */
  onMessage(event) {
    if (event.name === "reporterRealTime") {
      this.log(event.content);
      return;
    }
    if (event.origin === "debugger" && event.name === "start") {
      this.log(chalk.yellow(event.params.introMessage));
      this._inDebugMode = true;
      return this._inDebugMode;
    }
    if (event.origin === "debugger" && event.name === "stop") {
      this._inDebugMode = false;
      return this._inDebugMode;
    }
    if (event.name === "testFrameworkInit") {
      return this.emit("job:start", event.content);
    }
    if (event.name === "snapshot") {
      const snapshotResults = event.content;
      return snapshotResults.forEach((snapshotResult) => {
        this.#snapshotManager.add(snapshotResult);
      });
    }
    if (event.name === "error") {
      return this.log(
        `[${event.cid}]`,
        chalk.white(chalk.bgRed(chalk.bold(" Error: "))),
        event.content ? event.content.message || event.content.stack || event.content : ""
      );
    }
    if (event.origin !== "reporter" && event.origin !== "debugger") {
      if (EVENT_FILTER.includes(event.name)) {
        return;
      }
      return this.log(event.cid, event.origin, event.name, event.content);
    }
    if (event.name === "printFailureMessage") {
      return this.onTestError(event.content);
    }
    if (!this._messages[event.origin][event.name]) {
      this._messages[event.origin][event.name] = [];
    }
    this._messages[event.origin][event.name].push(event.content);
  }
  sigintTrigger() {
    if (this._inDebugMode) {
      return false;
    }
    const isRunning = this._jobs.size !== 0 || this._isWatchMode;
    const shutdownMessage = isRunning ? "Ending WebDriver sessions gracefully ...\n(press ctrl+c again to hard kill the runner)" : "Ended WebDriver sessions gracefully after a SIGINT signal was received!";
    return this.log("\n\n" + shutdownMessage);
  }
  printReporters() {
    const reporter = this._messages.reporter;
    this._messages.reporter = {};
    for (const [reporterName, messages] of Object.entries(reporter)) {
      this.log("\n", chalk.bold(chalk.magenta(`"${reporterName}" Reporter:`)));
      this.log(messages.join(""));
    }
  }
  printSummary() {
    const totalJobs = this.totalWorkerCnt - this.result.retries;
    const elapsed = new Date(Date.now() - this._start.getTime()).toUTCString().match(/(\d\d:\d\d:\d\d)/)[0];
    const retries = this.result.retries ? chalk.yellow(this.result.retries, "retries") + ", " : "";
    const failed = this.result.failed ? chalk.red(this.result.failed, "failed") + ", " : "";
    const skipped = this._skippedSpecs > 0 ? chalk.gray(this._skippedSpecs, "skipped") + ", " : "";
    const percentCompleted = totalJobs ? Math.round(this.result.finished / totalJobs * 100) : 0;
    const snapshotSummary = this.#snapshotManager.summary;
    const snapshotNotes = [];
    if (snapshotSummary.added > 0) {
      snapshotNotes.push(chalk.green(`${snapshotSummary.added} snapshot(s) added.`));
    }
    if (snapshotSummary.updated > 0) {
      snapshotNotes.push(chalk.yellow(`${snapshotSummary.updated} snapshot(s) updated.`));
    }
    if (snapshotSummary.unmatched > 0) {
      snapshotNotes.push(chalk.red(`${snapshotSummary.unmatched} snapshot(s) unmatched.`));
    }
    if (snapshotSummary.unchecked > 0) {
      snapshotNotes.push(chalk.gray(`${snapshotSummary.unchecked} snapshot(s) unchecked.`));
    }
    if (snapshotNotes.length > 0) {
      this.log("\nSnapshot Summary:");
      snapshotNotes.forEach((note) => this.log(note));
    }
    return this.log(
      "\nSpec Files:	",
      chalk.green(this.result.passed, "passed") + ", " + retries + failed + skipped + totalJobs,
      "total",
      `(${percentCompleted}% completed)`,
      "in",
      elapsed,
      this.#hasShard() ? `
Shard:		 ${this._config.shard.current} / ${this._config.shard.total}` : "",
      "\n"
    );
  }
  finalise() {
    this.printReporters();
    this.printSummary();
  }
};

// src/launcher.ts
var log3 = logger3("@wdio/cli:launcher");
var Launcher = class {
  constructor(_configFilePath, _args = {}, _isWatchMode = false) {
    this._configFilePath = _configFilePath;
    this._args = _args;
    this._isWatchMode = _isWatchMode;
    this.configParser = new ConfigParser2(this._configFilePath, this._args);
  }
  _configFilePath;
  _args;
  _isWatchMode;
  #isInitialized = false;
  configParser;
  isMultiremote = false;
  isParallelMultiremote = false;
  runner;
  interface;
  _exitCode = 0;
  _hasTriggeredExitRoutine = false;
  _schedule = [];
  _rid = [];
  _runnerStarted = 0;
  _runnerFailed = 0;
  _launcher;
  _resolve;
  /**
   * run sequence
   * @return  {Promise}  that only gets resolved with either an exitCode or an error
   */
  async run() {
    await this.initialize();
    const config3 = this.configParser.getConfig();
    const capabilities = this.configParser.getCapabilities();
    this.isParallelMultiremote = Array.isArray(capabilities) && capabilities.length > 0 && capabilities.every((cap) => Object.values(cap).length > 0 && Object.values(cap).every((c) => typeof c === "object" && c.capabilities));
    this.isMultiremote = this.isParallelMultiremote || !Array.isArray(capabilities);
    validateConfig(TESTRUNNER_DEFAULTS, { ...config3, capabilities });
    await enableFileLogging(config3.outputDir);
    logger3.setLogLevelsConfig(config3.logLevels, config3.logLevel);
    const [runnerName, runnerOptions] = Array.isArray(config3.runner) ? config3.runner : [config3.runner, {}];
    const Runner = (await initializePlugin(runnerName, "runner")).default;
    this.runner = new Runner(runnerOptions, config3);
    exitHook(this._exitHandler.bind(this));
    let exitCode = 0;
    let error = void 0;
    const caps = this.configParser.getCapabilities();
    try {
      const { ignoredWorkerServices, launcherServices } = await initializeLauncherService(config3, caps);
      this._launcher = launcherServices;
      this._args.ignoredWorkerServices = ignoredWorkerServices;
      await this.runner.initialize();
      log3.info("Run onPrepare hook");
      await runLauncherHook(config3.onPrepare, config3, caps);
      await runServiceHook(this._launcher, "onPrepare", config3, caps);
      const totalWorkerCnt = Array.isArray(capabilities) ? capabilities.map((c) => {
        if (this.isParallelMultiremote) {
          const keys = Object.keys(c);
          const caps2 = c[keys[0]].capabilities;
          return this.configParser.getSpecs(caps2["wdio:specs"], caps2["wdio:exclude"]).length;
        }
        const standaloneCaps = c;
        const cap = "alwaysMatch" in standaloneCaps ? standaloneCaps.alwaysMatch : standaloneCaps;
        return this.configParser.getSpecs(cap["wdio:specs"], cap["wdio:exclude"]).length;
      }).reduce((a, b) => a + b, 0) : 1;
      this.interface = new WDIOCLInterface(config3, totalWorkerCnt, this._isWatchMode);
      config3.runnerEnv.FORCE_COLOR = Number(this.interface.hasAnsiSupport).toString();
      await Promise.all([
        setupDriver(config3, caps),
        setupBrowser(config3, caps)
      ]);
      exitCode = await this._runMode(config3, caps);
      await logger3.waitForBuffer();
      this.interface.finalise();
    } catch (err) {
      error = err;
    } finally {
      if (!this._hasTriggeredExitRoutine) {
        this._hasTriggeredExitRoutine = true;
        const passesCodeCoverage = await this.runner.shutdown();
        if (!passesCodeCoverage) {
          exitCode = exitCode || 1;
        }
      }
      exitCode = await this.#runOnCompleteHook(config3, caps, exitCode);
    }
    if (error) {
      if (this.interface) {
        this.interface.logHookError(error);
      }
      throw error;
    }
    return exitCode;
  }
  /**
   * initialize launcher by loading `tsx` if needed
   */
  async initialize() {
    if (this.#isInitialized) {
      return;
    }
    const tsxPath = resolve("tsx", import.meta.url);
    if (!process.env.NODE_OPTIONS || !process.env.NODE_OPTIONS.includes(tsxPath)) {
      const moduleLoaderFlag = nodeVersion("major") >= 21 || nodeVersion("major") === 20 && nodeVersion("minor") >= 6 || nodeVersion("major") === 18 && nodeVersion("minor") >= 19 ? "--import" : "--loader";
      process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ""} ${moduleLoaderFlag} ${tsxPath}`;
    }
    if (TS_FILE_EXTENSIONS.some((ext) => this._configFilePath.endsWith(ext))) {
      await import(tsxPath);
    }
    this.#isInitialized = true;
    await this.configParser.initialize(this._args);
  }
  /**
   * run onComplete hook
   * Even if it fails we still want to see result and end logger stream.
   * Also ensure that user hooks are run before service hooks so that e.g.
   * a user can use plugin service, e.g. shared store service is still
   * available running hooks in this order
   */
  async #runOnCompleteHook(config3, caps, exitCode) {
    log3.info("Run onComplete hook");
    const onCompleteResults = await runOnCompleteHook(config3.onComplete, config3, caps, exitCode, this.interface?.result || { finished: 0, passed: 0, retries: 0, failed: 0 });
    if (this._launcher) {
      await runServiceHook(this._launcher, "onComplete", exitCode, config3, caps);
    }
    return onCompleteResults.includes(1) ? 1 : exitCode;
  }
  /**
   * run without triggering onPrepare/onComplete hooks
   */
  _runMode(config3, caps) {
    if (
      /**
       * no caps were provided
       */
      !caps || /**
       * capability array is empty
       */
      Array.isArray(caps) && caps.length === 0 || /**
       * user wants to use multiremote but capability object is empty
       */
      !Array.isArray(caps) && Object.keys(caps).length === 0
    ) {
      return new Promise((resolve2) => {
        log3.error("Missing capabilities, exiting with failure");
        return resolve2(1);
      });
    }
    const specFileRetries = this._isWatchMode ? 0 : -1;
    let cid = 0;
    if (this.isMultiremote && !this.isParallelMultiremote) {
      this._schedule.push({
        cid: cid++,
        caps,
        specs: this._formatSpecs(caps, specFileRetries),
        availableInstances: config3.maxInstances || 1,
        runningInstances: 0
      });
    } else {
      for (const capabilities of caps) {
        const availableInstances = this.isParallelMultiremote ? config3.maxInstances || 1 : config3.runner === "browser" ? 1 : capabilities["wdio:maxInstances"] || config3.maxInstancesPerCapability || DEFAULT_MAX_INSTANCES_PER_CAPABILITY_VALUE;
        this._schedule.push({
          cid: cid++,
          caps: capabilities,
          specs: this._formatSpecs(capabilities, specFileRetries),
          availableInstances,
          runningInstances: 0
        });
      }
    }
    return new Promise((resolve2) => {
      this._resolve = resolve2;
      if (Object.values(this._schedule).reduce((specCnt, schedule) => specCnt + schedule.specs.length, 0) === 0) {
        const { total, current } = config3.shard;
        if (total > 1) {
          log3.info(`No specs to execute in shard ${current}/${total}, exiting!`);
          return resolve2(0);
        }
        log3.error("No specs found to run, exiting with failure");
        return resolve2(1);
      }
      if (this._runSpecs()) {
        resolve2(0);
      }
    });
  }
  /**
   * Format the specs into an array of objects with files and retries
   */
  _formatSpecs(capabilities, specFileRetries) {
    let caps;
    if ("alwaysMatch" in capabilities) {
      caps = capabilities.alwaysMatch;
    } else if (typeof Object.keys(capabilities)[0] === "object" && "capabilities" in capabilities[Object.keys(capabilities)[0]]) {
      caps = {};
    } else {
      caps = capabilities;
    }
    const specs = (
      // @ts-expect-error deprecated
      caps.specs || caps["wdio:specs"]
    );
    const excludes = (
      // @ts-expect-error deprecated
      caps.exclude || caps["wdio:exclude"]
    );
    const files = this.configParser.getSpecs(specs, excludes);
    return files.map((file) => {
      if (typeof file === "string") {
        return { files: [file], retries: specFileRetries };
      } else if (Array.isArray(file)) {
        return { files: file, retries: specFileRetries };
      }
      log3.warn("Unexpected entry in specs that is neither string nor array: ", file);
      return { files: [], retries: specFileRetries };
    });
  }
  /**
   * run multiple single remote tests
   * @return {Boolean} true if all specs have been run and all instances have finished
   */
  _runSpecs() {
    if (this._hasTriggeredExitRoutine) {
      return true;
    }
    const config3 = this.configParser.getConfig();
    while (this._getNumberOfRunningInstances() < config3.maxInstances) {
      const schedulableCaps = this._schedule.filter((session) => {
        const filter = typeof config3.bail !== "number" || config3.bail < 1 || config3.bail > this._runnerFailed;
        if (!filter) {
          this._schedule.forEach((t) => {
            t.specs = [];
          });
          return false;
        }
        if (this._getNumberOfRunningInstances() >= config3.maxInstances) {
          return false;
        }
        return session.availableInstances > 0 && session.specs.length > 0;
      }).sort((a, b) => a.runningInstances - b.runningInstances);
      if (schedulableCaps.length === 0) {
        break;
      }
      const specs = schedulableCaps[0].specs.shift();
      this._startInstance(
        specs.files,
        schedulableCaps[0].caps,
        schedulableCaps[0].cid,
        specs.rid,
        specs.retries
      );
      schedulableCaps[0].availableInstances--;
      schedulableCaps[0].runningInstances++;
    }
    return this._getNumberOfRunningInstances() === 0 && this._getNumberOfSpecsLeft() === 0;
  }
  /**
   * gets number of all running instances
   * @return {number} number of running instances
   */
  _getNumberOfRunningInstances() {
    return this._schedule.map((a) => a.runningInstances).reduce((a, b) => a + b);
  }
  /**
   * get number of total specs left to complete whole suites
   * @return {number} specs left to complete suite
   */
  _getNumberOfSpecsLeft() {
    return this._schedule.map((a) => a.specs.length).reduce((a, b) => a + b);
  }
  /**
   * Start instance in a child process.
   * @param  {Array} specs  Specs to run
   * @param  {number} cid  Capabilities ID
   * @param  {string} rid  Runner ID override
   * @param  {number} retries  Number of retries remaining
   */
  async _startInstance(specs, caps, cid, rid, retries) {
    if (!this.runner || !this.interface) {
      throw new Error("Internal Error: no runner initialized, call run() first");
    }
    const config3 = this.configParser.getConfig();
    if (typeof config3.specFileRetriesDelay === "number" && config3.specFileRetries > 0 && config3.specFileRetries !== retries) {
      await sleep(config3.specFileRetriesDelay * 1e3);
    }
    const runnerId = rid || this._getRunnerId(cid);
    const processNumber = this._runnerStarted + 1;
    const debugArgs = [];
    let debugType;
    let debugHost = "";
    const debugPort = process.debugPort;
    for (const arg of process.execArgv) {
      const debugArgs2 = arg.match("--(debug|inspect)(?:-brk)?(?:=(.*):)?");
      if (debugArgs2) {
        const [, type, host] = debugArgs2;
        if (type) {
          debugType = type;
        }
        if (host) {
          debugHost = `${host}:`;
        }
      }
    }
    if (debugType) {
      debugArgs.push(`--${debugType}=${debugHost}${debugPort + processNumber}`);
    }
    const capExecArgs = [...config3.execArgv || []];
    const defaultArgs = capExecArgs.length ? process.execArgv : [];
    const execArgv = [...defaultArgs, ...debugArgs, ...capExecArgs];
    this._runnerStarted++;
    const workerCaps = structuredClone(caps);
    log3.info("Run onWorkerStart hook");
    await runLauncherHook(config3.onWorkerStart, runnerId, workerCaps, specs, this._args, execArgv).catch((error) => this._workerHookError(error));
    await runServiceHook(this._launcher, "onWorkerStart", runnerId, workerCaps, specs, this._args, execArgv).catch((error) => this._workerHookError(error));
    const worker = await this.runner.run({
      cid: runnerId,
      command: "run",
      configFile: this._configFilePath,
      args: {
        ...this._args,
        /**
         * Pass on user and key values to ensure they are available in the worker process when using
         * environment variables that were locally exported but not part of the environment.
         */
        user: config3.user,
        key: config3.key
      },
      caps: workerCaps,
      specs,
      execArgv,
      retries
    });
    worker.on("message", this.interface.onMessage.bind(this.interface));
    worker.on("error", this.interface.onMessage.bind(this.interface));
    worker.on("exit", (code) => {
      if (!this.configParser.getConfig().groupLogsByTestSpec) {
        return;
      }
      if (code.exitCode === 0) {
        console.log(WORKER_GROUPLOGS_MESSAGES.normalExit(code.cid));
      } else {
        console.log(WORKER_GROUPLOGS_MESSAGES.exitWithError(code.cid));
      }
      worker.logsAggregator.forEach((logLine) => {
        console.log(logLine.replace(new RegExp("\\n$"), ""));
      });
    });
    worker.on("exit", this._endHandler.bind(this));
  }
  _workerHookError(error) {
    if (!this.interface) {
      throw new Error("Internal Error: no interface initialized, call run() first");
    }
    this.interface.logHookError(error);
    if (this._resolve) {
      this._resolve(1);
    }
  }
  /**
   * generates a runner id
   * @param  {number} cid capability id (unique identifier for a capability)
   * @return {String}     runner id (combination of cid and test id e.g. 0a, 0b, 1a, 1b ...)
   */
  _getRunnerId(cid) {
    if (!this._rid[cid]) {
      this._rid[cid] = 0;
    }
    return `${cid}-${this._rid[cid]++}`;
  }
  /**
   * Close test runner process once all child processes have exited
   * @param  {number} cid       Capabilities ID
   * @param  {number} exitCode  exit code of child process
   * @param  {Array} specs      Specs that were run
   * @param  {number} retries   Number or retries remaining
   */
  async _endHandler({ cid: rid, exitCode, specs, retries }) {
    const passed = this._isWatchModeHalted() || exitCode === 0;
    if (!passed && retries > 0) {
      const requeue = this.configParser.getConfig().specFileRetriesDeferred ? "push" : "unshift";
      this._schedule[parseInt(rid, 10)].specs[requeue]({ files: specs, retries: retries - 1, rid });
    } else {
      this._exitCode = this._isWatchModeHalted() ? 0 : this._exitCode || exitCode;
      this._runnerFailed += !passed ? 1 : 0;
    }
    if (!this._isWatchModeHalted() && this.interface) {
      this.interface.emit("job:end", { cid: rid, passed, retries });
    }
    const cid = parseInt(rid, 10);
    this._schedule[cid].availableInstances++;
    this._schedule[cid].runningInstances--;
    log3.info("Run onWorkerEnd hook");
    const config3 = this.configParser.getConfig();
    await runLauncherHook(config3.onWorkerEnd, rid, exitCode, specs, retries).catch((error) => this._workerHookError(error));
    await runServiceHook(this._launcher, "onWorkerEnd", rid, exitCode, specs, retries).catch((error) => this._workerHookError(error));
    const shouldRunSpecs = this._runSpecs();
    const inWatchMode = this._isWatchMode && !this._hasTriggeredExitRoutine;
    if (!shouldRunSpecs || inWatchMode) {
      if (inWatchMode) {
        this.interface?.finalise();
      }
      return;
    }
    if (this._resolve) {
      this._resolve(passed ? this._exitCode : 1);
    }
  }
  /**
   * We need exitHandler to catch SIGINT / SIGTERM events.
   * Make sure all started selenium sessions get closed properly and prevent
   * having dead driver processes. To do so let the runner end its Selenium
   * session first before killing
   */
  _exitHandler(callback) {
    if (!callback || !this.runner || !this.interface) {
      return;
    }
    if (this._hasTriggeredExitRoutine) {
      return callback(true);
    }
    this._hasTriggeredExitRoutine = true;
    this.interface.sigintTrigger();
    return this.runner.shutdown().then(callback);
  }
  /**
   * returns true if user stopped watch mode, ex with ctrl+c
   * @returns {boolean}
   */
  _isWatchModeHalted() {
    return this._isWatchMode && this._hasTriggeredExitRoutine;
  }
};
var launcher_default = Launcher;

// src/run.ts
import path3 from "node:path";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";

// src/commands/index.ts
import { config as config2 } from "create-wdio/config/cli";
import { install } from "create-wdio/install/cli";

// src/commands/repl.ts
var repl_exports = {};
__export(repl_exports, {
  builder: () => builder2,
  cmdArgs: () => cmdArgs2,
  command: () => command2,
  desc: () => desc2,
  handler: () => handler2
});
import pickBy3 from "lodash.pickby";
import { remote } from "webdriverio";

// src/commands/run.ts
var run_exports = {};
__export(run_exports, {
  builder: () => builder,
  cmdArgs: () => cmdArgs,
  command: () => command,
  desc: () => desc,
  handler: () => handler,
  launch: () => launch,
  launchWithStdin: () => launchWithStdin
});
import fs from "node:fs/promises";
import path2 from "node:path";

// src/watcher.ts
import url from "node:url";
import path from "node:path";
import chokidar from "chokidar";
import pickBy2 from "lodash.pickby";
import flattenDeep from "lodash.flattendeep";
import union from "lodash.union";
import logger4 from "@wdio/logger";
var log4 = logger4("@wdio/cli:watch");
var Watcher = class {
  constructor(_configFile, _args) {
    this._configFile = _configFile;
    this._args = _args;
    log4.info("Starting launcher in watch mode");
    this._launcher = new launcher_default(this._configFile, this._args, true);
  }
  _configFile;
  _args;
  _launcher;
  _specs = [];
  async watch() {
    await this._launcher.initialize();
    const specs = this._launcher.configParser.getSpecs();
    const capSpecs = this._launcher.isMultiremote ? [] : union(flattenDeep(
      this._launcher.configParser.getCapabilities().map((cap) => "alwaysMatch" in cap ? cap.alwaysMatch["wdio:specs"] : cap["wdio:specs"] || [])
    ));
    this._specs = [...specs, ...capSpecs];
    const flattenedSpecs = flattenDeep(this._specs).map((fileUrl) => url.fileURLToPath(fileUrl));
    chokidar.watch(flattenedSpecs, { ignoreInitial: true }).on("add", this.getFileListener()).on("change", this.getFileListener());
    const { filesToWatch } = this._launcher.configParser.getConfig();
    if (filesToWatch.length) {
      const rootDir = path.dirname(path.resolve(process.cwd(), this._configFile));
      const globbedFilesToWatch = filesToWatch.map((file) => {
        const absolutePath = path.isAbsolute(file) ? path.normalize(file) : path.resolve(rootDir, file);
        return absolutePath;
      });
      chokidar.watch(globbedFilesToWatch, { ignoreInitial: true }).on("add", this.getFileListener(false)).on("change", this.getFileListener(false));
    }
    await this._launcher.run();
    const workers = this.getWorkers();
    Object.values(workers).forEach((worker) => worker.on("exit", () => {
      if (Object.values(workers).find((w) => w.isBusy)) {
        return;
      }
      this._launcher.interface?.finalise();
    }));
  }
  /**
   * return file listener callback that calls `run` method
   * @param  {Boolean}  [passOnFile=true]  if true pass on file change as parameter
   * @return {Function}                    chokidar event callback
   */
  getFileListener(passOnFile = true) {
    return (spec) => {
      const runSpecs = [];
      let singleSpecFound = false;
      for (let index = 0, length = this._specs.length; index < length; index += 1) {
        const value = this._specs[index];
        if (Array.isArray(value) && value.indexOf(spec) > -1) {
          runSpecs.push(value);
        } else if (!singleSpecFound && spec === value) {
          singleSpecFound = true;
          runSpecs.push(value);
        }
      }
      if (runSpecs.length === 0) {
        runSpecs.push(url.pathToFileURL(spec).href);
      }
      const { spec: _, ...args } = this._args;
      return runSpecs.map((spec2) => {
        return this.run({
          ...args,
          ...passOnFile ? { spec: [spec2] } : {}
        });
      });
    };
  }
  /**
   * helper method to get workers from worker pool of wdio runner
   * @param  predicate         filter by property value (see lodash.pickBy)
   * @param  includeBusyWorker don't filter out busy worker (default: false)
   * @return                   Object with workers, e.g. {'0-0': { ... }}
   */
  getWorkers(predicate, includeBusyWorker) {
    if (!this._launcher.runner) {
      throw new Error("Internal Error: no runner initialized, call run() first");
    }
    let workers = this._launcher.runner.workerPool;
    if (typeof predicate === "function") {
      workers = pickBy2(workers, predicate);
    }
    if (!includeBusyWorker) {
      workers = pickBy2(workers, (worker) => !worker.isBusy);
    }
    return workers;
  }
  /**
   * run workers with params
   * @param  params parameters to run the worker with
   */
  run(params = {}) {
    const workers = this.getWorkers(
      params.spec ? (worker) => Boolean(worker.specs.find((s) => params.spec?.includes(s))) : void 0
    );
    if (Object.keys(workers).length === 0 || !this._launcher.interface) {
      return;
    }
    this._launcher.interface.totalWorkerCnt = Object.entries(workers).length;
    this.cleanUp();
    for (const [, worker] of Object.entries(workers)) {
      const { cid, capabilities, specs, sessionId } = worker;
      const { hostname, path: path4, port, protocol, automationProtocol } = worker.config;
      const args = Object.assign({ sessionId, baseUrl: worker.config.baseUrl, hostname, path: path4, port, protocol, automationProtocol }, params);
      worker.postMessage("run", args);
      this._launcher.interface.emit("job:start", { cid, caps: capabilities, specs });
    }
  }
  cleanUp() {
    this._launcher.interface?.setup();
  }
};

// src/commands/run.ts
import { config } from "create-wdio/config/cli";
import { ConfigParser as ConfigParser3 } from "@wdio/config/node";
import logger5 from "@wdio/logger";
var log5 = logger5("@wdio/cli:run");
var command = "run <configPath>";
var desc = "Run your WDIO configuration file to initialize your tests. (default)";
var cmdArgs = {
  watch: {
    desc: "Run WebdriverIO in watch mode",
    type: "boolean"
  },
  hostname: {
    alias: "h",
    desc: "automation driver host address",
    type: "string"
  },
  port: {
    alias: "p",
    desc: "automation driver port",
    type: "number"
  },
  path: {
    type: "string",
    desc: 'path to WebDriver endpoints (default "/")'
  },
  user: {
    alias: "u",
    desc: "username if using a cloud service as automation backend",
    type: "string"
  },
  key: {
    alias: "k",
    desc: "corresponding access key to the user",
    type: "string"
  },
  logLevel: {
    alias: "l",
    desc: "level of logging verbosity",
    choices: ["trace", "debug", "info", "warn", "error", "silent"]
  },
  bail: {
    desc: "stop test runner after specific amount of tests have failed",
    type: "number"
  },
  baseUrl: {
    desc: "shorten url command calls by setting a base url",
    type: "string"
  },
  waitforTimeout: {
    alias: "w",
    desc: "timeout for all waitForXXX commands",
    type: "number"
  },
  updateSnapshots: {
    alias: "s",
    desc: "update DOM, image or test snapshots",
    type: "string",
    coerce: (value) => {
      if (value === "") {
        return "all";
      }
      return value;
    }
  },
  framework: {
    alias: "f",
    desc: "defines the framework (Mocha, Jasmine or Cucumber) to run the specs",
    type: "string"
  },
  reporters: {
    alias: "r",
    desc: "reporters to print out the results on stdout",
    type: "array"
  },
  suite: {
    desc: "overwrites the specs attribute and runs the defined suite",
    type: "array"
  },
  spec: {
    desc: "run only a certain spec file or wildcard - overrides specs piped from stdin",
    type: "array"
  },
  exclude: {
    desc: "exclude certain spec file or wildcard from the test run - overrides exclude piped from stdin",
    type: "array"
  },
  "repeat": {
    desc: "Repeat specific specs and/or suites N times",
    type: "number"
  },
  mochaOpts: {
    desc: "Mocha options",
    coerce: coerceOptsFor("mocha")
  },
  jasmineOpts: {
    desc: "Jasmine options",
    coerce: coerceOptsFor("jasmine")
  },
  cucumberOpts: {
    desc: "Cucumber options",
    coerce: coerceOptsFor("cucumber")
  },
  coverage: {
    desc: "Enable coverage for browser runner"
  },
  headless: {
    desc: "run all browser instances in headless mode, overrides capability settings in wdio.conf.js",
    type: "boolean"
  },
  shard: {
    desc: "Shard tests and execute only the selected shard. Specify in the one-based form like `--shard x/y`, where x is the current and y the total shard.",
    coerce: (shard) => {
      const [current, total] = shard.split("/").map(Number);
      if (Number.isNaN(current) || Number.isNaN(total)) {
        throw new Error("Shard parameter must be in the form `x/y`, where x and y are positive integers.");
      }
      return { current, total };
    }
  }
};
var builder = (yargs2) => {
  return yargs2.options(cmdArgs).example("$0 run wdio.conf.js --suite foobar", 'Run suite on testsuite "foobar"').example("$0 run wdio.conf.js --spec ./tests/e2e/a.js --spec ./tests/e2e/b.js", "Run suite on specific specs").example("$0 run wdio.conf.js --shard 1/4", "Run only the first shard of 4 shards").example("$0 run wdio.conf.js --headless", "Run all tests in headless mode").example("$0 run wdio.conf.js --headless=false", "Run all tests in headed (non-headless) mode").example("$0 run wdio.conf.js --mochaOpts.timeout 60000", "Run suite with custom Mocha timeout").example("$0 run wdio.conf.js --tsConfigPath=./configs/bdd-tsconfig.json", "Run suite with tsx using custom tsconfig.json").epilogue(CLI_EPILOGUE).help();
};
function launchWithStdin(wdioConfPath, params) {
  let stdinData = "";
  process.stdin.resume();
  const stdin = process.stdin;
  stdin.setEncoding("utf8");
  stdin.on("data", (data) => {
    stdinData += data;
  });
  stdin.on("end", () => {
    if (stdinData.length > 0) {
      params.spec = stdinData.trim().split(/\r?\n/);
    }
    launch(wdioConfPath, params);
  });
}
async function launch(wdioConfPath, params) {
  const launcher = new launcher_default(wdioConfPath, params);
  return launcher.run().then((...args) => {
    if (!process.env.WDIO_UNIT_TESTS) {
      process.exit(...args);
    }
  }).catch((err) => {
    console.error(err);
    if (!process.env.WDIO_UNIT_TESTS) {
      process.exit(1);
    }
  });
}
async function handler(argv) {
  const { configPath = "wdio.conf.js", ...params } = argv;
  const wdioConf = await config.formatConfigFilePaths(configPath);
  const confAccess = await config.canAccessConfigPath(wdioConf.fullPathNoExtension, wdioConf.fullPath);
  if (!confAccess) {
    try {
      await config.missingConfigurationPrompt("run", wdioConf.fullPathNoExtension);
      if (process.env.WDIO_UNIT_TESTS) {
        return;
      }
      return handler(argv);
    } catch {
      process.exit(1);
    }
  }
  const tsConfigPathFromEnvVar = process.env.TSCONFIG_PATH && path2.resolve(process.cwd(), process.env.TSCONFIG_PATH) || process.env.TSX_TSCONFIG_PATH && path2.resolve(process.cwd(), process.env.TSX_TSCONFIG_PATH);
  const tsConfigPathFromParams = params.tsConfigPath && path2.resolve(process.cwd(), params.tsConfigPath);
  const tsConfigPathRelativeToWdioConfig = path2.join(path2.dirname(confAccess), "tsconfig.json");
  if (TS_FILE_EXTENSIONS.some((ext) => confAccess.endsWith(ext))) {
    const { resolve: resolve2 } = await import("import-meta-resolve");
    const tsxPath = resolve2("tsx", import.meta.url);
    await import(tsxPath);
  }
  const localTSConfigPath = tsConfigPathFromEnvVar || tsConfigPathFromParams || await tsConfigPathFromConfigFile(confAccess, params) || tsConfigPathRelativeToWdioConfig;
  const hasLocalTSConfig = await fs.access(localTSConfigPath).then(() => true, () => false);
  if (hasLocalTSConfig) {
    process.env.TSX_TSCONFIG_PATH = localTSConfigPath;
  }
  if (params.watch) {
    const watcher = new Watcher(confAccess, params);
    return watcher.watch();
  }
  if (process.stdin.isTTY || !process.stdout.isTTY) {
    return launch(confAccess, params);
  }
  launchWithStdin(confAccess, params);
}
async function tsConfigPathFromConfigFile(wdioConfPath, params) {
  try {
    const configParser = new ConfigParser3(wdioConfPath, params);
    await configParser.initialize();
    const { tsConfigPath } = configParser.getConfig();
    if (tsConfigPath) {
      return tsConfigPath;
    }
  } catch {
    log5.debug(`Unable to parse config file. If tsConfigPath is set in ${wdioConfPath}, it will be ignored.`);
    return;
  }
  return;
}

// src/commands/repl.ts
var IGNORED_ARGS = [
  "bail",
  "framework",
  "reporters",
  "suite",
  "spec",
  "exclude",
  "mochaOpts",
  "jasmineOpts",
  "cucumberOpts"
];
var command2 = "repl <option> [capabilities]";
var desc2 = "Run WebDriver session in command line";
var cmdArgs2 = {
  platformVersion: {
    alias: "v",
    desc: "Version of OS for mobile devices",
    type: "string"
  },
  deviceName: {
    alias: "d",
    desc: "Device name for mobile devices",
    type: "string"
  },
  udid: {
    alias: "u",
    desc: "UDID of real mobile devices",
    type: "string"
  }
};
var builder2 = (yargs2) => {
  return yargs2.options(pickBy3({ ...cmdArgs2, ...cmdArgs }, (_, key) => !IGNORED_ARGS.includes(key))).example("$0 repl firefox --path /", "Run repl locally").example("$0 repl chrome -u <SAUCE_USERNAME> -k <SAUCE_ACCESS_KEY>", "Run repl in Sauce Labs cloud").example("$0 repl android", "Run repl browser on launched Android device").example('$0 repl "./path/to/your_app.app"', "Run repl native app on iOS simulator").example('$0 repl ios -v 11.3 -d "iPhone 7" -u 123432abc', "Run repl browser on iOS device with capabilities").example('$0 repl "./path/to/wdio.config.js" 0 -p 9515', "Run repl using the first capability from the capabilty array in wdio.config.js").example('$0 repl "./path/to/wdio.config.js" "myChromeBrowser" -p 9515', "Run repl using a named multiremote capabilities in wdio.config.js").epilogue(CLI_EPILOGUE).help();
};
var handler2 = async (argv) => {
  const caps = await getCapabilities(argv);
  const client = await remote({ ...argv, ...caps });
  global.$ = client.$.bind(client);
  global.$$ = client.$$.bind(client);
  global.browser = client;
  await client.debug();
  return client.deleteSession();
};

// src/commands/index.ts
var commands = [config2, install, repl_exports, run_exports];

// src/run.ts
var DEFAULT_CONFIG_FILENAME = "wdio.conf.js";
var DESCRIPTION = [
  "The `wdio` command allows you run and manage your WebdriverIO test suite.",
  "If no command is provided it calls the `run` command by default, so:",
  "",
  "$ wdio wdio.conf.js",
  "",
  "is the same as:",
  "$ wdio run wdio.conf.js",
  "",
  "For more information, visit: https://webdriver.io/docs/clioptions"
];
async function run() {
  const argv = yargs(hideBin(process.argv)).command(commands).example("wdio run wdio.conf.js --suite foobar", 'Run suite on testsuite "foobar"').example("wdio run wdio.conf.js --spec ./tests/e2e/a.js --spec ./tests/e2e/b.js", "Run suite on specific specs").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5", "Run scenario by line number").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5:10", "Run scenarios by line number").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5:10 --spec ./test/e2e/b.feature", "Run scenarios by line number in single feature and another complete feature").example("wdio run wdio.conf.js --tsConfigPath=./configs/bdd-tsconfig.json", "Run suite with tsx using custom tsconfig.json").example("wdio install reporter spec", "Install @wdio/spec-reporter").example("wdio repl chrome -u <SAUCE_USERNAME> -k <SAUCE_ACCESS_KEY>", "Run repl in Sauce Labs cloud").updateStrings({ "Commands:": `${DESCRIPTION.join("\n")}

Commands:` }).version(pkg.version).epilogue(CLI_EPILOGUE);
  if (!process.argv.find((arg) => arg === "--help")) {
    argv.options(cmdArgs);
  }
  const params = await argv.parse();
  if (!params._ || params._.find((param) => SUPPORTED_COMMANDS.includes(param))) {
    return;
  }
  const args = {
    ...params,
    configPath: path3.resolve(process.cwd(), params._[0] && params._[0].toString() || DEFAULT_CONFIG_FILENAME)
  };
  try {
    const cp = await handler(args);
    return cp;
  } catch (err) {
    const output = await new Promise(
      (resolve2) => yargs(hideBin(process.argv)).parse("--help", (err2, argv2, output2) => resolve2(output2))
    );
    console.error(`${output}

${err.stack}`);
    if (!process.env.WDIO_UNIT_TESTS) {
      process.exit(1);
    }
  }
}
export {
  launcher_default as Launcher,
  run
};