create-wdio 9.32.0 → 10.0.0-alpha.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,6 @@ import path from "node:path";
24
24
  import util, { promisify } from "node:util";
25
25
  import fs from "node:fs/promises";
26
26
  import { execSync } from "node:child_process";
27
- import readDir from "recursive-readdir";
28
27
  import { $ } from "execa";
29
28
  import { readPackageUp } from "read-pkg-up";
30
29
 
@@ -124,17 +123,380 @@ function getInstallCommand(pm, packages, dev) {
124
123
  return `${pm} ${installCommand[pm]} ${packages.join(" ")} ${devParam}`;
125
124
  }
126
125
 
126
+ // src/answerFlags.ts
127
+ var AnswerFlagError = class extends Error {
128
+ name = "AnswerFlagError";
129
+ };
130
+ var HASH = "$--$";
131
+ function packageChoices(packages, key = (short) => short) {
132
+ return () => Object.fromEntries(packages().map(({ value }) => {
133
+ const [pkg, short, purpose] = value.split(HASH);
134
+ return [key(short, purpose, pkg), value];
135
+ }));
136
+ }
137
+ var ANSWER_FLAGS = [{
138
+ name: "runner",
139
+ answer: "runner",
140
+ type: "string",
141
+ desc: "Type of testing",
142
+ choices: packageChoices(() => SUPPORTED_PACKAGES.runner, (_, purpose) => purpose)
143
+ }, {
144
+ name: "preset",
145
+ answer: "preset",
146
+ type: "string",
147
+ desc: "UI framework of your components (with --runner component)",
148
+ choices: () => Object.fromEntries(SUPPORTED_BROWSER_RUNNER_PRESETS.map(({ name, value }) => [
149
+ value === null ? "other" : value.split(HASH)[1] || name.split(" ")[0].toLowerCase(),
150
+ value
151
+ ]))
152
+ }, {
153
+ name: "testingLibrary",
154
+ answer: "installTestingLibrary",
155
+ type: "boolean",
156
+ desc: "Install Testing Library (with --preset vue, svelte, solid, react or preact)"
157
+ }, {
158
+ name: "desktopFramework",
159
+ answer: "desktopFramework",
160
+ type: "string",
161
+ desc: "Desktop app framework (with --runner desktop)",
162
+ choices: () => ({
163
+ electron: "Electron (https://www.electronjs.org/)" /* Electron */,
164
+ tauri: "Tauri (https://tauri.app/)" /* Tauri */,
165
+ dioxus: "Dioxus (https://dioxuslabs.com/)" /* Dioxus */,
166
+ macos: "Native macOS app" /* MacOS */
167
+ })
168
+ }, {
169
+ name: "electronBuildTool",
170
+ answer: "electronBuildTool",
171
+ type: "string",
172
+ desc: "Tool that builds your Electron app (with --desktop-framework electron)",
173
+ choices: () => ({
174
+ forge: "Electron Forge (https://www.electronforge.io/)" /* ElectronForge */,
175
+ builder: "electron-builder (https://www.electron.build/)" /* ElectronBuilder */,
176
+ other: "Something else" /* SomethingElse */
177
+ })
178
+ }, {
179
+ name: "electronAppBinaryPath",
180
+ answer: "electronAppBinaryPath",
181
+ type: "string",
182
+ desc: "Path to the built Electron app (with --electron-build-tool other)"
183
+ }, {
184
+ name: "tauriDriverProvider",
185
+ answer: "tauriDriverProvider",
186
+ type: "string",
187
+ desc: "WebDriver provider for Tauri (with --desktop-framework tauri)",
188
+ choices: () => ({
189
+ official: "Official tauri-driver (cross-platform, Cargo-installed)" /* Official */,
190
+ crabnebula: "CrabNebula tauri-driver (managed cloud / drop-in)" /* CrabNebula */,
191
+ embedded: "Embedded driver via tauri-plugin-wdio-webdriver crate" /* Embedded */
192
+ })
193
+ }, {
194
+ name: "tauriFrontendPlugin",
195
+ answer: "tauriUseFrontendPlugin",
196
+ type: "boolean",
197
+ desc: "Install @wdio/tauri-plugin (with --desktop-framework tauri)"
198
+ }, {
199
+ name: "tauriAppBinaryPath",
200
+ answer: "tauriAppBinaryPath",
201
+ type: "string",
202
+ desc: "Path to the built Tauri binary (with --desktop-framework tauri)"
203
+ }, {
204
+ name: "dioxusDriverProvider",
205
+ answer: "dioxusDriverProvider",
206
+ type: "string",
207
+ desc: "WebDriver provider for Dioxus (with --desktop-framework dioxus)",
208
+ choices: () => ({
209
+ embedded: "Embedded in-process driver (recommended \u2014 no external driver, all platforms)" /* Embedded */,
210
+ external: "External driver via wdio-dioxus-driver (Windows only)" /* External */
211
+ })
212
+ }, {
213
+ name: "dioxusAppBinaryPath",
214
+ answer: "dioxusAppBinaryPath",
215
+ type: "string",
216
+ desc: "Path to the Dioxus debug binary (with --desktop-framework dioxus)"
217
+ }, {
218
+ name: "backend",
219
+ answer: "backend",
220
+ type: "string",
221
+ desc: "Where the automation backend runs (with --runner e2e)",
222
+ choices: () => ({
223
+ local: "On my local machine" /* Local */,
224
+ experitest: "In the cloud using Experitest" /* Experitest */,
225
+ saucelabs: "In the cloud using Sauce Labs" /* Saucelabs */,
226
+ browserstack: "In the cloud using BrowserStack" /* Browserstack */,
227
+ other: "In the cloud using Testingbot or LambdaTest or a different service" /* OtherVendors */,
228
+ grid: "I have my own Selenium cloud" /* Grid */
229
+ })
230
+ }, {
231
+ name: "environment",
232
+ answer: "e2eEnvironment",
233
+ type: "string",
234
+ desc: "What to automate (with --runner e2e). Implied by --browsers or --mobile-environment",
235
+ choices: () => ({ web: "web", mobile: "mobile" })
236
+ }, {
237
+ name: "mobileEnvironment",
238
+ answer: "mobileEnvironment",
239
+ type: "string",
240
+ desc: "Mobile platform (with --environment mobile)",
241
+ choices: () => ({ android: "android", ios: "ios" })
242
+ }, {
243
+ name: "browsers",
244
+ answer: "browserEnvironment",
245
+ type: "array",
246
+ desc: "Comma-separated browsers to start with (with --environment web)",
247
+ choices: () => ({ chrome: "chrome", firefox: "firefox", safari: "safari", edge: "MicrosoftEdge" })
248
+ }, {
249
+ name: "hostname",
250
+ answer: "hostname",
251
+ type: "string",
252
+ desc: "Host of the cloud service or Selenium server (with --backend other or grid)"
253
+ }, {
254
+ name: "port",
255
+ answer: "port",
256
+ type: "string",
257
+ desc: "Port of the cloud service or Selenium server (with --backend other or grid)"
258
+ }, {
259
+ name: "path",
260
+ answer: "path",
261
+ type: "string",
262
+ desc: "Path of the Selenium server (with --backend grid)"
263
+ }, {
264
+ name: "envUser",
265
+ answer: "env_user",
266
+ type: "string",
267
+ desc: "Environment variable for the cloud username (with --backend saucelabs or browserstack, or other with a lambdatest.com --hostname)"
268
+ }, {
269
+ name: "envKey",
270
+ answer: "env_key",
271
+ type: "string",
272
+ desc: "Environment variable for the cloud access key (with --backend saucelabs or browserstack, or other with a lambdatest.com --hostname)"
273
+ }, {
274
+ name: "region",
275
+ answer: "region",
276
+ type: "string",
277
+ desc: "Sauce Labs region (with --backend saucelabs)",
278
+ choices: () => Object.fromEntries(Object.values(RegionOptions).map((region) => [region, region]))
279
+ }, {
280
+ name: "sauceConnect",
281
+ answer: "useSauceConnect",
282
+ type: "boolean",
283
+ desc: "Set up Sauce Connect for a local app (with --backend saucelabs)"
284
+ }, {
285
+ name: "expAccessKey",
286
+ answer: "expEnvAccessKey",
287
+ type: "string",
288
+ desc: "Experitest access key (with --backend experitest)"
289
+ }, {
290
+ name: "expHostname",
291
+ answer: "expEnvHostname",
292
+ type: "string",
293
+ desc: "Experitest cloud host (with --backend experitest)"
294
+ }, {
295
+ name: "expPort",
296
+ answer: "expEnvPort",
297
+ type: "string",
298
+ desc: "Experitest cloud port (with --backend experitest)"
299
+ }, {
300
+ name: "expProtocol",
301
+ answer: "expEnvProtocol",
302
+ type: "string",
303
+ desc: "Experitest protocol (with --backend experitest and a port other than 80 or 443)",
304
+ choices: () => ({ http: "http", https: "https" })
305
+ }, {
306
+ name: "framework",
307
+ answer: "framework",
308
+ type: "string",
309
+ desc: "Test framework",
310
+ choices: packageChoices(() => SUPPORTED_PACKAGES.framework, (short, purpose, pkg) => pkg.startsWith("@serenity-js") ? `serenity-${purpose}` : short)
311
+ }, {
312
+ name: "typescript",
313
+ answer: "isUsingTypeScript",
314
+ type: "boolean",
315
+ desc: "Write tests in TypeScript"
316
+ }, {
317
+ name: "generateTestFiles",
318
+ answer: "generateTestFiles",
319
+ type: "boolean",
320
+ desc: "Generate example test files"
321
+ }, {
322
+ name: "specs",
323
+ answer: "specs",
324
+ type: "string",
325
+ desc: "Location of spec or feature files (with --generate-test-files)"
326
+ }, {
327
+ name: "stepDefinitions",
328
+ answer: "stepDefinitions",
329
+ type: "string",
330
+ desc: "Location of step definitions (with --framework cucumber)"
331
+ }, {
332
+ name: "pageObjects",
333
+ answer: "usePageObjects",
334
+ type: "boolean",
335
+ desc: "Generate page objects (with --runner e2e and --generate-test-files)"
336
+ }, {
337
+ name: "pages",
338
+ answer: "pages",
339
+ type: "string",
340
+ desc: "Location of page objects (with --page-objects)"
341
+ }, {
342
+ name: "serenityLibPath",
343
+ answer: "serenityLibPath",
344
+ type: "string",
345
+ desc: "Location of the Serenity/JS Screenplay library (with --framework serenity-*)"
346
+ }, {
347
+ name: "agentSupport",
348
+ answer: "agentSupport",
349
+ type: "boolean",
350
+ desc: "Write the AGENTS.md section and the wdio-session skill"
351
+ }, {
352
+ name: "reporters",
353
+ answer: "reporters",
354
+ type: "array",
355
+ desc: "Comma-separated reporters",
356
+ choices: packageChoices(() => SUPPORTED_PACKAGES.reporter)
357
+ }, {
358
+ name: "plugins",
359
+ answer: "plugins",
360
+ type: "array",
361
+ desc: "Comma-separated plugins",
362
+ choices: packageChoices(() => SUPPORTED_PACKAGES.plugin)
363
+ }, {
364
+ name: "visualTesting",
365
+ answer: "includeVisualTesting",
366
+ type: "boolean",
367
+ desc: "Add visual testing (with --runner e2e or component)"
368
+ }, {
369
+ name: "services",
370
+ answer: "services",
371
+ type: "array",
372
+ desc: "Comma-separated services, replaces the ones the wizard picks for your setup",
373
+ choices: packageChoices(() => SUPPORTED_PACKAGES.service)
374
+ }, {
375
+ name: "npmInstall",
376
+ answer: "npmInstall",
377
+ type: "boolean",
378
+ desc: "Install the dependencies"
379
+ }];
380
+ function toKebabCase(name) {
381
+ return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
382
+ }
383
+ function flagFor(answer) {
384
+ return ANSWER_FLAGS.find((flag) => flag.answer === answer);
385
+ }
386
+ function describeChoices(flag) {
387
+ return Object.keys(flag.choices()).join(", ");
388
+ }
389
+ function describeAnswerFlag(flag) {
390
+ return flag.choices ? `${flag.desc}: ${describeChoices(flag)}` : flag.desc;
391
+ }
392
+ var ANSWER_FLAGS_HELP = [
393
+ "Every wizard question has a flag. A flag answers its question and the wizard",
394
+ "asks the rest, or uses the defaults for the rest with --yes. Boolean flags",
395
+ "take a --no- prefix, e.g. --no-typescript. A flag for a question the wizard",
396
+ "does not ask for your setup is an error, and so is a value it does not offer."
397
+ ].join("\n");
398
+ function getAnswerFlagYargsOptions() {
399
+ return Object.fromEntries(ANSWER_FLAGS.map((flag) => [toKebabCase(flag.name), {
400
+ desc: describeAnswerFlag(flag),
401
+ type: flag.type === "boolean" ? "boolean" : "string",
402
+ group: "Wizard answers:"
403
+ }]));
404
+ }
405
+ function lastValue(raw) {
406
+ return Array.isArray(raw) ? raw[raw.length - 1] : raw;
407
+ }
408
+ function toAnswerValue(flag, value) {
409
+ if (!flag.choices) {
410
+ return value;
411
+ }
412
+ const choices = flag.choices();
413
+ const key = value.toLowerCase();
414
+ if (!Object.prototype.hasOwnProperty.call(choices, key)) {
415
+ throw new AnswerFlagError(
416
+ `Invalid value "${value}" for --${toKebabCase(flag.name)}. Use one of: ${describeChoices(flag)}`
417
+ );
418
+ }
419
+ return choices[key];
420
+ }
421
+ function parseAnswerFlags(argv) {
422
+ const answers = {};
423
+ for (const flag of ANSWER_FLAGS) {
424
+ const raw = argv[flag.name];
425
+ if (typeof raw === "undefined") {
426
+ continue;
427
+ }
428
+ if (flag.type === "boolean") {
429
+ answers[flag.answer] = raw === true || raw === "true";
430
+ } else if (flag.type === "array") {
431
+ const values = [raw].flat().flatMap((value) => String(value).split(",")).map((value) => value.trim()).filter(Boolean).map((value) => toAnswerValue(flag, value));
432
+ answers[flag.answer] = [...new Set(values)];
433
+ } else {
434
+ answers[flag.answer] = toAnswerValue(flag, String(lastValue(raw)));
435
+ }
436
+ }
437
+ if (!answers.e2eEnvironment) {
438
+ if (answers.mobileEnvironment) {
439
+ answers.e2eEnvironment = "mobile";
440
+ } else if (answers.browserEnvironment) {
441
+ answers.e2eEnvironment = "web";
442
+ }
443
+ }
444
+ return answers;
445
+ }
446
+ function isAsked(question, answers) {
447
+ const when = question.when;
448
+ if (typeof when === "function") {
449
+ return Boolean(when(answers));
450
+ }
451
+ return typeof when === "undefined" || Boolean(when);
452
+ }
453
+ function choiceValues(question, answers) {
454
+ if (!question.choices) {
455
+ return void 0;
456
+ }
457
+ const choices = typeof question.choices === "function" ? question.choices(answers) : question.choices;
458
+ return choices.filter((choice) => typeof choice !== "undefined").map((choice) => choice && typeof choice === "object" ? choice.value : choice);
459
+ }
460
+ function assertAnswerFlagsApply(flagAnswers, answers) {
461
+ for (const [answer, value] of Object.entries(flagAnswers)) {
462
+ const flag = flagFor(answer);
463
+ const usage = `--${toKebabCase(flag.name)}`;
464
+ const questions = QUESTIONNAIRE.filter((question) => question.name === answer && isAsked(question, answers));
465
+ if (questions.length === 0) {
466
+ throw new AnswerFlagError(`${usage} does not apply to this setup. ${flag.desc}.`);
467
+ }
468
+ const offered = questions.map((question) => choiceValues(question, answers));
469
+ if (offered.some((values) => typeof values === "undefined")) {
470
+ continue;
471
+ }
472
+ const allowed = new Set(offered.flat());
473
+ const rejected = [value].flat().filter((item) => !allowed.has(item));
474
+ if (rejected.length === 0) {
475
+ continue;
476
+ }
477
+ const cliValues = Object.entries(flag.choices());
478
+ const toCli = (item) => cliValues.find(([, v]) => v === item)?.[0] ?? String(item);
479
+ throw new AnswerFlagError(
480
+ `${usage} ${rejected.map(toCli).join(",")} is not available for this setup. Use one of: ${[...new Set([...allowed].map(toCli))].join(", ")}`
481
+ );
482
+ }
483
+ }
484
+
127
485
  // src/utils.ts
128
486
  var NPM_COMMAND = /^win/.test(process.platform) ? "npm.cmd" : "npm";
129
487
  var __dirname = path.dirname(url.fileURLToPath(import.meta.url));
130
488
  process.on("SIGINT", () => printAndExit(void 0, "SIGINT"));
131
489
  var TEMPLATE_ROOT_DIR = process.env.WDIO_UNIT_TESTS ? path.join(__dirname, "templates", "exampleFiles") : path.join(__dirname, "..", "templates", "exampleFiles");
132
- function runProgram(command2, args, options) {
490
+ function runProgram(command2, args, options, passUsageError = false) {
133
491
  const child = spawn(command2, args, { stdio: "inherit", ...options });
134
492
  return new Promise((resolve, rejects) => {
135
493
  let error;
136
494
  child.on("error", (e) => error = e);
137
495
  child.on("close", (code, signal) => {
496
+ if (passUsageError && code === 2) {
497
+ process.exit(2);
498
+ return rejects(`Usage error calling: ${command2} ${args.join(" ")}`);
499
+ }
138
500
  if (code !== 0) {
139
501
  const errorMessage = error && error.message || `Error calling: ${command2} ${args.join(" ")}`;
140
502
  printAndExit(errorMessage, signal);
@@ -158,24 +520,35 @@ function convertPackageHashToObject(pkg, hash = "$--$") {
158
520
  const [p, short, purpose] = pkg.split(hash);
159
521
  return { package: p, short, purpose };
160
522
  }
161
- async function getAnswers(yes) {
162
- if (yes) {
163
- const ignoredQuestions = ["e2eEnvironment"];
164
- const filteredQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name));
165
- const answers2 = {};
166
- for (const question of filteredQuestionaire) {
167
- if (question.when && !question.when(answers2)) {
168
- continue;
169
- }
170
- Object.assign(answers2, {
171
- [question.name]: typeof question.default !== "undefined" ? typeof question.default === "function" ? await question.default(answers2) : question.default : question.choices && question.choices.length ? typeof question.choices === "function" ? question.choices(answers2)[0].value ? question.choices(answers2)[0].value : question.choices(answers2)[0] : question.choices[0].value ? question.type === "checkbox" ? [question.choices[0].value] : question.choices[0].value : question.choices[0] : {}
172
- });
523
+ async function getAnswers(yes, flagAnswers = {}) {
524
+ const answers = yes ? await getDefaultAnswers(flagAnswers) : await promptAnswers(flagAnswers);
525
+ assertAnswerFlagsApply(flagAnswers, answers);
526
+ return answers;
527
+ }
528
+ async function getDefaultAnswers(flagAnswers) {
529
+ const ignoredQuestions = ["e2eEnvironment"];
530
+ const filteredQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name));
531
+ const answers = { ...flagAnswers };
532
+ for (const question of filteredQuestionaire) {
533
+ if (Object.prototype.hasOwnProperty.call(flagAnswers, question.name)) {
534
+ continue;
535
+ }
536
+ if (question.when && !question.when(answers)) {
537
+ continue;
173
538
  }
174
- answers2.isUsingTypeScript = await answers2.isUsingTypeScript;
175
- answers2.specs = await answers2.specs;
176
- answers2.pages = await answers2.pages;
177
- return answers2;
539
+ if (typeof question.default === "undefined" && !question.choices) {
540
+ continue;
541
+ }
542
+ Object.assign(answers, {
543
+ [question.name]: typeof question.default !== "undefined" ? typeof question.default === "function" ? await question.default(answers) : question.default : question.choices && question.choices.length ? typeof question.choices === "function" ? question.choices(answers)[0].value ? question.choices(answers)[0].value : question.choices(answers)[0] : question.choices[0].value ? question.type === "checkbox" ? [question.choices[0].value] : question.choices[0].value : question.choices[0] : {}
544
+ });
178
545
  }
546
+ answers.isUsingTypeScript = await answers.isUsingTypeScript;
547
+ answers.specs = await answers.specs;
548
+ answers.pages = await answers.pages;
549
+ return answers;
550
+ }
551
+ async function promptAnswers(flagAnswers) {
179
552
  const projectProps = await getProjectProps(process.cwd());
180
553
  const isProjectExisting = Boolean(projectProps);
181
554
  const nameInPackageJsonIsNotCreateWdioDefault = projectProps?.packageJson?.name !== "my-new-project";
@@ -211,7 +584,7 @@ async function getAnswers(yes) {
211
584
  if (answers.createPackageJSON) {
212
585
  answers.projectRoot = process.cwd();
213
586
  }
214
- return inquirer.prompt(QUESTIONNAIRE, answers);
587
+ return inquirer.prompt(QUESTIONNAIRE, { ...answers, ...flagAnswers });
215
588
  }
216
589
  async function getProjectProps(cwd = process.cwd()) {
217
590
  try {
@@ -267,11 +640,8 @@ async function createPackageJSON(parsedAnswers) {
267
640
  return;
268
641
  }
269
642
  if (parsedAnswers.createPackageJSON === false) {
270
- if (!packageJsonExists) {
271
- console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`);
272
- return !process.env.WDIO_UNIT_TESTS && process.exit(0);
273
- }
274
- return;
643
+ console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`);
644
+ process.exit(0);
275
645
  }
276
646
  if (parsedAnswers.createPackageJSON) {
277
647
  console.log(`Creating a ${chalk.bold("package.json")} for the directory...`);
@@ -318,10 +688,13 @@ async function setupTypeScript(parsedAnswers) {
318
688
  const config = {
319
689
  compilerOptions: {
320
690
  // compiler
321
- moduleResolution: "node",
322
- module: !parsedAnswers.esmSupport ? "commonjs" : "ESNext",
323
- target: "es2022",
324
- lib: ["es2022", "dom"],
691
+ // TypeScript 6 deprecates `moduleResolution: 'node'` (node10). ESM uses
692
+ // `bundler`, which resolves imports as `tsx` does; CommonJS uses `NodeNext`,
693
+ // which follows the `type` of the project's package.json.
694
+ moduleResolution: parsedAnswers.esmSupport ? "bundler" : "NodeNext",
695
+ module: parsedAnswers.esmSupport ? "ESNext" : "NodeNext",
696
+ target: "es2024",
697
+ lib: ["es2024", "dom"],
325
698
  types,
326
699
  skipLibCheck: true,
327
700
  // bundler
@@ -466,8 +839,10 @@ function getPreset(parsedAnswers) {
466
839
  function addServiceDeps(names, packages, update = false) {
467
840
  if (names.some(({ short }) => short === "appium")) {
468
841
  const result = execSync("appium --version || echo APPIUM_MISSING", { stdio: "pipe" }).toString().trim();
469
- if (result === "APPIUM_MISSING") {
470
- packages.push("appium");
842
+ const major = Number.parseInt(result.replace(/^v/i, "").split(".")[0] || "", 10);
843
+ const hasAppium3 = result !== "APPIUM_MISSING" && !Number.isNaN(major) && major >= 3;
844
+ if (!hasAppium3) {
845
+ packages.push("appium@^3");
471
846
  } else if (update) {
472
847
  console.log(
473
848
  "\n=======",
@@ -527,10 +902,15 @@ async function createWDIOScript(parsedAnswers) {
527
902
  console.log(chalk.green(chalk.bold("\u2714 Success!")));
528
903
  return true;
529
904
  }
530
- async function runAppiumInstaller(parsedAnswers) {
905
+ async function runAppiumInstaller(parsedAnswers, yes = false) {
531
906
  if (parsedAnswers.e2eEnvironment !== "mobile") {
532
907
  return;
533
908
  }
909
+ if (yes || !process.stdin.isTTY) {
910
+ return console.log(
911
+ "Skipping the interactive Appium installer. Run `npx appium-installer` later, or see https://appium.io/docs/en/latest/quickstart/"
912
+ );
913
+ }
534
914
  const answer = await inquirer.prompt({
535
915
  name: "continueWithAppiumSetup",
536
916
  message: "Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?",
@@ -626,6 +1006,10 @@ async function generateTestFiles(answers) {
626
1006
  }
627
1007
  return generateBrowserRunnerTestFiles(answers);
628
1008
  }
1009
+ async function readDir(dir) {
1010
+ const entries = await fs.readdir(dir, { recursive: true, withFileTypes: true });
1011
+ return entries.filter((entry) => entry.isFile()).map((entry) => path.join(entry.parentPath, entry.name));
1012
+ }
629
1013
  async function generateSerenityExamples(answers) {
630
1014
  const templateDirectories = Object.entries({
631
1015
  [answers.destSpecRootPath]: path.join(TEMPLATE_ROOT_DIR, "serenity-js", answers.serenityAdapter),
@@ -650,10 +1034,7 @@ async function generateLocalRunnerTestFiles(answers) {
650
1034
  if (answers.usePageObjects) {
651
1035
  testFiles.push(path.join(TEMPLATE_ROOT_DIR, "pageobjects"));
652
1036
  }
653
- const files = (await Promise.all(testFiles.map((dirPath) => readDir(
654
- dirPath,
655
- [(file, stats) => !stats.isDirectory() && !(file.endsWith(".ejs") || file.endsWith(".feature"))]
656
- )))).reduce((cur, acc) => [...acc, ...cur], []);
1037
+ const files = (await Promise.all(testFiles.map(readDir))).flat().filter((file) => file.endsWith(".ejs") || file.endsWith(".feature"));
657
1038
  await Promise.all(files.map(async (file) => {
658
1039
  const renderedTpl = await renderFile(file, { answers });
659
1040
  const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset);
@@ -692,7 +1073,7 @@ async function formatConfigFilePaths(config) {
692
1073
  // package.json
693
1074
  var package_default = {
694
1075
  name: "create-wdio",
695
- version: "9.31.6",
1076
+ version: "9.32.0",
696
1077
  description: "Install and setup a WebdriverIO project with all its dependencies in a single run",
697
1078
  author: "WebdriverIO Team <mail@webdriver.io>",
698
1079
  homepage: "https://github.com/webdriverio/webdriverio/tree/main/packages/create-wdio",
@@ -721,9 +1102,9 @@ var package_default = {
721
1102
  }
722
1103
  },
723
1104
  types: "./build/index.d.ts",
724
- typeScriptVersion: "3.8.3",
1105
+ typeScriptVersion: "6.0.3",
725
1106
  engines: {
726
- node: ">=18.20.0"
1107
+ node: ">=22.19.0"
727
1108
  },
728
1109
  repository: {
729
1110
  type: "git",
@@ -753,10 +1134,9 @@ var package_default = {
753
1134
  inquirer: "^12.7.0",
754
1135
  "normalize-package-data": "^7.0.0",
755
1136
  "read-pkg-up": "^10.1.0",
756
- "recursive-readdir": "^2.2.3",
757
1137
  semver: "^7.6.3",
758
1138
  "type-fest": "^4.41.0",
759
- yargs: "^17.7.2"
1139
+ yargs: "^18.2.0"
760
1140
  },
761
1141
  devDependencies: {
762
1142
  "@types/cross-spawn": "^6.0.6",
@@ -784,7 +1164,7 @@ var PROGRAM_TITLE = `
784
1164
  `;
785
1165
  var UNSUPPORTED_NODE_VERSION = `\u26A0\uFE0F Unsupported Node.js Version Error \u26A0\uFE0F
786
1166
  You are using Node.js ${process.version} which is too old to be used with WebdriverIO.
787
- Please update to Node.js v20 to continue.
1167
+ Please update to Node.js v22.19.0 to continue.
788
1168
  `;
789
1169
  var INSTALL_COMMAND = {
790
1170
  npm: "install",
@@ -846,6 +1226,7 @@ var SUPPORTED_PACKAGES = {
846
1226
  service: [
847
1227
  // internal or community driver services
848
1228
  { name: "visual", value: "@wdio/visual-service$--$visual" },
1229
+ { name: "ai", value: "@wdio/ai-service$--$ai" },
849
1230
  { name: "vite", value: "wdio-vite-service$--$vite" },
850
1231
  { name: "nuxt", value: "wdio-nuxt-service$--$nuxt" },
851
1232
  { name: "firefox-profile", value: "@wdio/firefox-profile-service$--$firefox-profile" },
@@ -1540,6 +1921,11 @@ var QUESTIONNAIRE = [{
1540
1921
  /* istanbul ignore next */
1541
1922
  (answers) => answers.generateTestFiles && usesSerenity(answers)
1542
1923
  )
1924
+ }, {
1925
+ type: "confirm",
1926
+ name: "agentSupport",
1927
+ message: "Set up coding agent support (AGENTS.md section and wdio-session skill)?",
1928
+ default: true
1543
1929
  }, {
1544
1930
  type: "checkbox",
1545
1931
  name: "reporters",
@@ -1677,10 +2063,79 @@ To install dependencies, execute:
1677
2063
  `;
1678
2064
 
1679
2065
  // src/cli/utils.ts
1680
- import path3 from "node:path";
1681
- import fs3 from "node:fs/promises";
2066
+ import path4 from "node:path";
2067
+ import fs4 from "node:fs/promises";
1682
2068
  import inquirer2 from "inquirer";
1683
- async function runConfigCommand(parsedAnswers, npmTag) {
2069
+
2070
+ // src/cli/agent.ts
2071
+ import fs3 from "node:fs";
2072
+ import path3 from "node:path";
2073
+ import { fileURLToPath } from "node:url";
2074
+ var AGENT_SECTION_HEADING = "## End-to-end tests (WebdriverIO v10)";
2075
+ var AGENT_SECTION = `${AGENT_SECTION_HEADING}
2076
+
2077
+ - Docs: https://webdriver.io/llms.txt - fetch the relevant page as Markdown (append \`.md\`) before using an API you are not sure about. Do not use APIs from WebdriverIO v8 or older.
2078
+ - Config: \`wdio.conf.ts\`. Specs: \`test/specs/**/*.e2e.ts\`. Page objects: \`test/pageobjects/\`.
2079
+ - Run all tests: \`npx wdio run wdio.conf.ts\`
2080
+ - Run a single spec: \`npx wdio run wdio.conf.ts --spec test/specs/login.e2e.ts\`
2081
+ - Tests are async: always \`await\` commands, e.g. \`await $('button').click()\`. Never use the removed sync mode.
2082
+ - Prefer user-facing selectors: accessibility name or text (\`$('aria/Submit')\`, \`$('button=Submit')\`), then \`data-testid\`. Avoid XPath and generated CSS classes.
2083
+ - Rely on auto-waiting and \`expect-webdriverio\` matchers (\`await expect($('h1')).toHaveText('Welcome')\`) instead of \`browser.pause()\`.
2084
+ - To explore the app or verify a selector, use the \`wdio-mcp\` MCP server.
2085
+ - To drive the app from the shell, follow \`.agents/skills/wdio-session/SKILL.md\` (\`npx wdio session\`).
2086
+ - When a test fails, read the DevTools trace in \`test-results/\` (see \`transcript.md\`) before changing code.
2087
+ `;
2088
+ function readIfExists(file) {
2089
+ return fs3.existsSync(file) ? fs3.readFileSync(file, "utf-8") : void 0;
2090
+ }
2091
+ function readSessionSkill(start = path3.dirname(fileURLToPath(import.meta.url))) {
2092
+ let dir = start;
2093
+ for (let i = 0; i < 8; i++) {
2094
+ const candidates = [
2095
+ path3.join(dir, "wdio-session", "src", "skill", "SKILL.md"),
2096
+ path3.join(dir, "packages", "wdio-session", "src", "skill", "SKILL.md"),
2097
+ path3.join(dir, "node_modules", "@wdio", "session", "src", "skill", "SKILL.md"),
2098
+ path3.join(dir, "templates", "wdio-session", "SKILL.md")
2099
+ ];
2100
+ for (const candidate of candidates) {
2101
+ const markdown = readIfExists(candidate);
2102
+ if (markdown) {
2103
+ return markdown;
2104
+ }
2105
+ }
2106
+ const parent = path3.dirname(dir);
2107
+ if (parent === dir) {
2108
+ break;
2109
+ }
2110
+ dir = parent;
2111
+ }
2112
+ throw new Error("Cannot find the wdio-session skill (src/skill/SKILL.md).");
2113
+ }
2114
+ function writeAgentSupport(root) {
2115
+ const skillDest = path3.join(root, ".agents", "skills", "wdio-session", "SKILL.md");
2116
+ if (!fs3.existsSync(skillDest)) {
2117
+ const markdown = readSessionSkill();
2118
+ fs3.mkdirSync(path3.dirname(skillDest), { recursive: true });
2119
+ fs3.writeFileSync(skillDest, markdown);
2120
+ }
2121
+ const agentsPath = path3.join(root, "AGENTS.md");
2122
+ const agents = readIfExists(agentsPath) || "";
2123
+ if (!agents.includes(AGENT_SECTION_HEADING)) {
2124
+ const sep = agents.length === 0 ? "" : agents.endsWith("\n") ? "\n" : "\n\n";
2125
+ fs3.writeFileSync(agentsPath, agents + sep + AGENT_SECTION);
2126
+ }
2127
+ const gitignorePath = path3.join(root, ".gitignore");
2128
+ const gitignore = readIfExists(gitignorePath) || "";
2129
+ const hasEntry = gitignore.split("\n").some((line) => line.trim() === ".wdio/session/" || line.trim() === ".wdio/session");
2130
+ if (!hasEntry) {
2131
+ const sep = gitignore.length === 0 || gitignore.endsWith("\n") ? "" : "\n";
2132
+ fs3.writeFileSync(gitignorePath, `${gitignore}${sep}.wdio/session/
2133
+ `);
2134
+ }
2135
+ }
2136
+
2137
+ // src/cli/utils.ts
2138
+ async function runConfigCommand(parsedAnswers, npmTag, yes = false) {
1684
2139
  console.log("\n");
1685
2140
  await createPackageJSON(parsedAnswers);
1686
2141
  await setupTypeScript(parsedAnswers);
@@ -1709,10 +2164,13 @@ async function runConfigCommand(parsedAnswers, npmTag) {
1709
2164
  extraInfo: extraInfoParts.join("\n")
1710
2165
  })
1711
2166
  );
1712
- await runAppiumInstaller(parsedAnswers);
2167
+ await runAppiumInstaller(parsedAnswers, yes);
2168
+ if (parsedAnswers.rawAnswers?.agentSupport) {
2169
+ writeAgentSupport(parsedAnswers.projectRootDir);
2170
+ }
1713
2171
  }
1714
2172
  async function missingConfigurationPrompt(command2, configPath, runConfigCmd = runConfigCommand) {
1715
- const message = `Could not execute "${command2}" due to missing configuration, file "${path3.parse(configPath).name}[.js/.ts]" not found! Would you like to create one?`;
2173
+ const message = `Could not execute "${command2}" due to missing configuration, file "${path4.parse(configPath).name}[.js/.ts]" not found! Would you like to create one?`;
1716
2174
  const { config } = await inquirer2.prompt({
1717
2175
  type: "confirm",
1718
2176
  name: "config",
@@ -1721,10 +2179,7 @@ async function missingConfigurationPrompt(command2, configPath, runConfigCmd = r
1721
2179
  });
1722
2180
  if (!config) {
1723
2181
  console.log(`No WebdriverIO configuration found in "${process.cwd()}"`);
1724
- if (!process.env.WDIO_UNIT_TESTS) {
1725
- process.exit(0);
1726
- }
1727
- return configPath;
2182
+ process.exit(0);
1728
2183
  }
1729
2184
  const parsedAnswers = await parseAnswers(false);
1730
2185
  await runConfigCmd(parsedAnswers, "latest");
@@ -1736,8 +2191,8 @@ function resolveTsConfigFilePath({
1736
2191
  specs,
1737
2192
  runnerShort
1738
2193
  }) {
1739
- const rootTsConfigPath = path3.resolve(projectRootDir, "tsconfig.json");
1740
- const dedicatedTsConfigPath = path3.resolve(
2194
+ const rootTsConfigPath = path4.resolve(projectRootDir, "tsconfig.json");
2195
+ const dedicatedTsConfigPath = path4.resolve(
1741
2196
  projectRootDir,
1742
2197
  `tsconfig.${runnerShort === "local" ? "e2e" : "wdio"}.json`
1743
2198
  );
@@ -1747,13 +2202,13 @@ function resolveTsConfigFilePath({
1747
2202
  if (!specs) {
1748
2203
  return dedicatedTsConfigPath;
1749
2204
  }
1750
- const specPathWithoutGlobs = specs.split(path3.sep).filter((segment) => !segment.includes("*")).join(path3.sep);
1751
- const candidate = path3.resolve(projectRootDir, path3.dirname(specPathWithoutGlobs), "tsconfig.json");
1752
- return path3.resolve(candidate) === rootTsConfigPath ? dedicatedTsConfigPath : candidate;
2205
+ const specPathWithoutGlobs = specs.split(path4.sep).filter((segment) => !segment.includes("*")).join(path4.sep);
2206
+ const candidate = path4.resolve(projectRootDir, path4.dirname(specPathWithoutGlobs), "tsconfig.json");
2207
+ return path4.resolve(candidate) === rootTsConfigPath ? dedicatedTsConfigPath : candidate;
1753
2208
  }
1754
- var parseAnswers = async function(yes) {
2209
+ var parseAnswers = async function(yes, flagAnswers = {}) {
1755
2210
  console.log(CONFIG_HELPER_INTRO);
1756
- const answers = await getAnswers(yes);
2211
+ const answers = await getAnswers(yes, flagAnswers);
1757
2212
  const frameworkPackage = convertPackageHashToObject(answers.framework);
1758
2213
  const runnerPackage = convertPackageHashToObject(answers.runner || SUPPORTED_PACKAGES.runner[0].value);
1759
2214
  const servicePackages = answers.services.map((service) => convertPackageHashToObject(service));
@@ -1772,7 +2227,7 @@ var parseAnswers = async function(yes) {
1772
2227
  ...servicePackages.map((service) => service.package),
1773
2228
  ...serenityPackages
1774
2229
  ].filter(Boolean);
1775
- const hasRootTSConfig = await fs3.access(path3.resolve(projectRootDir, "tsconfig.json")).then(() => true, () => false);
2230
+ const hasRootTSConfig = await fs4.access(path4.resolve(projectRootDir, "tsconfig.json")).then(() => true, () => false);
1776
2231
  const tsConfigFilePath = resolveTsConfigFilePath({
1777
2232
  projectRootDir,
1778
2233
  hasRootTSConfig,
@@ -1782,7 +2237,7 @@ var parseAnswers = async function(yes) {
1782
2237
  const parsedPaths = getPathForFileGeneration(answers, projectRootDir);
1783
2238
  const isUsingTypeScript = answers.isUsingTypeScript;
1784
2239
  const wdioConfigFilename = `wdio.conf.${isUsingTypeScript ? "ts" : "js"}`;
1785
- const wdioConfigPath = path3.resolve(projectRootDir, wdioConfigFilename);
2240
+ const wdioConfigPath = path4.resolve(projectRootDir, wdioConfigFilename);
1786
2241
  return {
1787
2242
  projectName: projectProps?.packageJson.name || "Test Suite",
1788
2243
  // default values required in templates
@@ -1801,8 +2256,8 @@ var parseAnswers = async function(yes) {
1801
2256
  reporters: reporterPackages.map(({ short }) => short),
1802
2257
  plugins: pluginPackages.map(({ short }) => short),
1803
2258
  services: servicePackages.map(({ short }) => short),
1804
- specs: answers.specs && `./${path3.relative(projectRootDir, answers.specs).replaceAll(path3.sep, "/")}`,
1805
- stepDefinitions: answers.stepDefinitions && `./${path3.relative(projectRootDir, answers.stepDefinitions).replaceAll(path3.sep, "/")}`,
2259
+ specs: answers.specs && `./${path4.relative(projectRootDir, answers.specs).replaceAll(path4.sep, "/")}`,
2260
+ stepDefinitions: answers.stepDefinitions && `./${path4.relative(projectRootDir, answers.stepDefinitions).replaceAll(path4.sep, "/")}`,
1806
2261
  packagesToInstall,
1807
2262
  isUsingTypeScript,
1808
2263
  esmSupport: projectProps && !projectProps.esmSupported ? false : true,
@@ -1817,20 +2272,20 @@ var parseAnswers = async function(yes) {
1817
2272
  relativePath: parsedPaths.relativePath,
1818
2273
  hasRootTSConfig,
1819
2274
  tsConfigFilePath,
1820
- tsProject: `./${path3.relative(projectRootDir, tsConfigFilePath).replaceAll(path3.sep, "/")}`,
2275
+ tsProject: `./${path4.relative(projectRootDir, tsConfigFilePath).replaceAll(path4.sep, "/")}`,
1821
2276
  wdioConfigPath
1822
2277
  };
1823
2278
  };
1824
2279
  async function canAccessConfigPath(configPathNoExtension, configPath) {
1825
2280
  return new Promise((resolve, reject) => {
1826
2281
  if (configPath) {
1827
- fs3.access(configPath).then(() => resolve(configPath), reject);
2282
+ fs4.access(configPath).then(() => resolve(configPath), reject);
1828
2283
  } else {
1829
2284
  reject();
1830
2285
  }
1831
2286
  }).catch(() => Promise.all(SUPPORTED_CONFIG_FILE_EXTENSION.map(async (supportedExtension) => {
1832
2287
  const configPathWithExtension = `${configPathNoExtension}.${supportedExtension}`;
1833
- return fs3.access(configPathWithExtension).then(() => configPathWithExtension, () => void 0);
2288
+ return fs4.access(configPathWithExtension).then(() => configPathWithExtension, () => void 0);
1834
2289
  })).then(
1835
2290
  (configFilePaths) => configFilePaths.find(Boolean),
1836
2291
  () => void 0
@@ -1867,11 +2322,23 @@ var cmdArgs = {
1867
2322
  }
1868
2323
  };
1869
2324
  var builder = (yargs) => {
1870
- return yargs.options(cmdArgs).epilogue(CLI_EPILOGUE).help();
2325
+ return yargs.options({ ...cmdArgs, ...getAnswerFlagYargsOptions() }).example("$0 config", "Answer every question in the wizard").example("$0 config --yes", "Use the defaults: Mocha, Chrome and page objects").example("$0 config --yes --framework cucumber --no-typescript", "Use the defaults, but Cucumber in JavaScript").example("$0 config --yes --environment mobile --mobile-environment android", "Android app tests with the Appium service").example("$0 config --runner component --preset react --framework mocha", "Answer some questions, the wizard asks the rest").epilogue(`${ANSWER_FLAGS_HELP}
2326
+
2327
+ ${CLI_EPILOGUE}`).help();
1871
2328
  };
1872
2329
  async function handler(argv, runConfigCmd = runConfigCommand) {
1873
- const parsedAnswers = await parseAnswers(argv.yes);
1874
- await runConfigCmd(parsedAnswers, argv.npmTag);
2330
+ let parsedAnswers;
2331
+ try {
2332
+ parsedAnswers = await parseAnswers(argv.yes, parseAnswerFlags(argv));
2333
+ } catch (err) {
2334
+ if (!(err instanceof AnswerFlagError)) {
2335
+ throw err;
2336
+ }
2337
+ console.error(`Error: ${err.message}
2338
+ Run "wdio config --help" for all flags.`);
2339
+ return process.exit(2);
2340
+ }
2341
+ await runConfigCmd(parsedAnswers, argv.npmTag, argv.yes);
1875
2342
  return {
1876
2343
  success: true,
1877
2344
  parsedAnswers,