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.
@@ -13,8 +13,8 @@ __export(install_exports, {
13
13
  desc: () => desc,
14
14
  handler: () => handler
15
15
  });
16
- import fs4 from "node:fs/promises";
17
- import path4 from "node:path";
16
+ import fs5 from "node:fs/promises";
17
+ import path5 from "node:path";
18
18
 
19
19
  // src/utils.ts
20
20
  import url from "node:url";
@@ -22,7 +22,6 @@ import path2 from "node:path";
22
22
  import util, { promisify } from "node:util";
23
23
  import fs2 from "node:fs/promises";
24
24
  import { execSync } from "node:child_process";
25
- import readDir from "recursive-readdir";
26
25
  import { $ } from "execa";
27
26
  import { readPackageUp } from "read-pkg-up";
28
27
 
@@ -85,7 +84,7 @@ import spawn from "cross-spawn";
85
84
  // package.json
86
85
  var package_default = {
87
86
  name: "create-wdio",
88
- version: "9.31.6",
87
+ version: "9.32.0",
89
88
  description: "Install and setup a WebdriverIO project with all its dependencies in a single run",
90
89
  author: "WebdriverIO Team <mail@webdriver.io>",
91
90
  homepage: "https://github.com/webdriverio/webdriverio/tree/main/packages/create-wdio",
@@ -114,9 +113,9 @@ var package_default = {
114
113
  }
115
114
  },
116
115
  types: "./build/index.d.ts",
117
- typeScriptVersion: "3.8.3",
116
+ typeScriptVersion: "6.0.3",
118
117
  engines: {
119
- node: ">=18.20.0"
118
+ node: ">=22.19.0"
120
119
  },
121
120
  repository: {
122
121
  type: "git",
@@ -146,10 +145,9 @@ var package_default = {
146
145
  inquirer: "^12.7.0",
147
146
  "normalize-package-data": "^7.0.0",
148
147
  "read-pkg-up": "^10.1.0",
149
- "recursive-readdir": "^2.2.3",
150
148
  semver: "^7.6.3",
151
149
  "type-fest": "^4.41.0",
152
- yargs: "^17.7.2"
150
+ yargs: "^18.2.0"
153
151
  },
154
152
  devDependencies: {
155
153
  "@types/cross-spawn": "^6.0.6",
@@ -177,7 +175,7 @@ var PROGRAM_TITLE = `
177
175
  `;
178
176
  var UNSUPPORTED_NODE_VERSION = `\u26A0\uFE0F Unsupported Node.js Version Error \u26A0\uFE0F
179
177
  You are using Node.js ${process.version} which is too old to be used with WebdriverIO.
180
- Please update to Node.js v20 to continue.
178
+ Please update to Node.js v22.19.0 to continue.
181
179
  `;
182
180
  var INSTALL_COMMAND = {
183
181
  npm: "install",
@@ -239,6 +237,7 @@ var SUPPORTED_PACKAGES = {
239
237
  service: [
240
238
  // internal or community driver services
241
239
  { name: "visual", value: "@wdio/visual-service$--$visual" },
240
+ { name: "ai", value: "@wdio/ai-service$--$ai" },
242
241
  { name: "vite", value: "wdio-vite-service$--$vite" },
243
242
  { name: "nuxt", value: "wdio-nuxt-service$--$nuxt" },
244
243
  { name: "firefox-profile", value: "@wdio/firefox-profile-service$--$firefox-profile" },
@@ -933,6 +932,11 @@ var QUESTIONNAIRE = [{
933
932
  /* istanbul ignore next */
934
933
  (answers) => answers.generateTestFiles && usesSerenity(answers)
935
934
  )
935
+ }, {
936
+ type: "confirm",
937
+ name: "agentSupport",
938
+ message: "Set up coding agent support (AGENTS.md section and wdio-session skill)?",
939
+ default: true
936
940
  }, {
937
941
  type: "checkbox",
938
942
  name: "reporters",
@@ -1111,17 +1115,326 @@ function getInstallCommand(pm, packages, dev) {
1111
1115
  return `${pm} ${installCommand[pm]} ${packages.join(" ")} ${devParam}`;
1112
1116
  }
1113
1117
 
1118
+ // src/answerFlags.ts
1119
+ var AnswerFlagError = class extends Error {
1120
+ name = "AnswerFlagError";
1121
+ };
1122
+ var HASH = "$--$";
1123
+ function packageChoices(packages, key = (short) => short) {
1124
+ return () => Object.fromEntries(packages().map(({ value }) => {
1125
+ const [pkg, short, purpose] = value.split(HASH);
1126
+ return [key(short, purpose, pkg), value];
1127
+ }));
1128
+ }
1129
+ var ANSWER_FLAGS = [{
1130
+ name: "runner",
1131
+ answer: "runner",
1132
+ type: "string",
1133
+ desc: "Type of testing",
1134
+ choices: packageChoices(() => SUPPORTED_PACKAGES.runner, (_, purpose) => purpose)
1135
+ }, {
1136
+ name: "preset",
1137
+ answer: "preset",
1138
+ type: "string",
1139
+ desc: "UI framework of your components (with --runner component)",
1140
+ choices: () => Object.fromEntries(SUPPORTED_BROWSER_RUNNER_PRESETS.map(({ name, value }) => [
1141
+ value === null ? "other" : value.split(HASH)[1] || name.split(" ")[0].toLowerCase(),
1142
+ value
1143
+ ]))
1144
+ }, {
1145
+ name: "testingLibrary",
1146
+ answer: "installTestingLibrary",
1147
+ type: "boolean",
1148
+ desc: "Install Testing Library (with --preset vue, svelte, solid, react or preact)"
1149
+ }, {
1150
+ name: "desktopFramework",
1151
+ answer: "desktopFramework",
1152
+ type: "string",
1153
+ desc: "Desktop app framework (with --runner desktop)",
1154
+ choices: () => ({
1155
+ electron: "Electron (https://www.electronjs.org/)" /* Electron */,
1156
+ tauri: "Tauri (https://tauri.app/)" /* Tauri */,
1157
+ dioxus: "Dioxus (https://dioxuslabs.com/)" /* Dioxus */,
1158
+ macos: "Native macOS app" /* MacOS */
1159
+ })
1160
+ }, {
1161
+ name: "electronBuildTool",
1162
+ answer: "electronBuildTool",
1163
+ type: "string",
1164
+ desc: "Tool that builds your Electron app (with --desktop-framework electron)",
1165
+ choices: () => ({
1166
+ forge: "Electron Forge (https://www.electronforge.io/)" /* ElectronForge */,
1167
+ builder: "electron-builder (https://www.electron.build/)" /* ElectronBuilder */,
1168
+ other: "Something else" /* SomethingElse */
1169
+ })
1170
+ }, {
1171
+ name: "electronAppBinaryPath",
1172
+ answer: "electronAppBinaryPath",
1173
+ type: "string",
1174
+ desc: "Path to the built Electron app (with --electron-build-tool other)"
1175
+ }, {
1176
+ name: "tauriDriverProvider",
1177
+ answer: "tauriDriverProvider",
1178
+ type: "string",
1179
+ desc: "WebDriver provider for Tauri (with --desktop-framework tauri)",
1180
+ choices: () => ({
1181
+ official: "Official tauri-driver (cross-platform, Cargo-installed)" /* Official */,
1182
+ crabnebula: "CrabNebula tauri-driver (managed cloud / drop-in)" /* CrabNebula */,
1183
+ embedded: "Embedded driver via tauri-plugin-wdio-webdriver crate" /* Embedded */
1184
+ })
1185
+ }, {
1186
+ name: "tauriFrontendPlugin",
1187
+ answer: "tauriUseFrontendPlugin",
1188
+ type: "boolean",
1189
+ desc: "Install @wdio/tauri-plugin (with --desktop-framework tauri)"
1190
+ }, {
1191
+ name: "tauriAppBinaryPath",
1192
+ answer: "tauriAppBinaryPath",
1193
+ type: "string",
1194
+ desc: "Path to the built Tauri binary (with --desktop-framework tauri)"
1195
+ }, {
1196
+ name: "dioxusDriverProvider",
1197
+ answer: "dioxusDriverProvider",
1198
+ type: "string",
1199
+ desc: "WebDriver provider for Dioxus (with --desktop-framework dioxus)",
1200
+ choices: () => ({
1201
+ embedded: "Embedded in-process driver (recommended \u2014 no external driver, all platforms)" /* Embedded */,
1202
+ external: "External driver via wdio-dioxus-driver (Windows only)" /* External */
1203
+ })
1204
+ }, {
1205
+ name: "dioxusAppBinaryPath",
1206
+ answer: "dioxusAppBinaryPath",
1207
+ type: "string",
1208
+ desc: "Path to the Dioxus debug binary (with --desktop-framework dioxus)"
1209
+ }, {
1210
+ name: "backend",
1211
+ answer: "backend",
1212
+ type: "string",
1213
+ desc: "Where the automation backend runs (with --runner e2e)",
1214
+ choices: () => ({
1215
+ local: "On my local machine" /* Local */,
1216
+ experitest: "In the cloud using Experitest" /* Experitest */,
1217
+ saucelabs: "In the cloud using Sauce Labs" /* Saucelabs */,
1218
+ browserstack: "In the cloud using BrowserStack" /* Browserstack */,
1219
+ other: "In the cloud using Testingbot or LambdaTest or a different service" /* OtherVendors */,
1220
+ grid: "I have my own Selenium cloud" /* Grid */
1221
+ })
1222
+ }, {
1223
+ name: "environment",
1224
+ answer: "e2eEnvironment",
1225
+ type: "string",
1226
+ desc: "What to automate (with --runner e2e). Implied by --browsers or --mobile-environment",
1227
+ choices: () => ({ web: "web", mobile: "mobile" })
1228
+ }, {
1229
+ name: "mobileEnvironment",
1230
+ answer: "mobileEnvironment",
1231
+ type: "string",
1232
+ desc: "Mobile platform (with --environment mobile)",
1233
+ choices: () => ({ android: "android", ios: "ios" })
1234
+ }, {
1235
+ name: "browsers",
1236
+ answer: "browserEnvironment",
1237
+ type: "array",
1238
+ desc: "Comma-separated browsers to start with (with --environment web)",
1239
+ choices: () => ({ chrome: "chrome", firefox: "firefox", safari: "safari", edge: "MicrosoftEdge" })
1240
+ }, {
1241
+ name: "hostname",
1242
+ answer: "hostname",
1243
+ type: "string",
1244
+ desc: "Host of the cloud service or Selenium server (with --backend other or grid)"
1245
+ }, {
1246
+ name: "port",
1247
+ answer: "port",
1248
+ type: "string",
1249
+ desc: "Port of the cloud service or Selenium server (with --backend other or grid)"
1250
+ }, {
1251
+ name: "path",
1252
+ answer: "path",
1253
+ type: "string",
1254
+ desc: "Path of the Selenium server (with --backend grid)"
1255
+ }, {
1256
+ name: "envUser",
1257
+ answer: "env_user",
1258
+ type: "string",
1259
+ desc: "Environment variable for the cloud username (with --backend saucelabs or browserstack, or other with a lambdatest.com --hostname)"
1260
+ }, {
1261
+ name: "envKey",
1262
+ answer: "env_key",
1263
+ type: "string",
1264
+ desc: "Environment variable for the cloud access key (with --backend saucelabs or browserstack, or other with a lambdatest.com --hostname)"
1265
+ }, {
1266
+ name: "region",
1267
+ answer: "region",
1268
+ type: "string",
1269
+ desc: "Sauce Labs region (with --backend saucelabs)",
1270
+ choices: () => Object.fromEntries(Object.values(RegionOptions).map((region) => [region, region]))
1271
+ }, {
1272
+ name: "sauceConnect",
1273
+ answer: "useSauceConnect",
1274
+ type: "boolean",
1275
+ desc: "Set up Sauce Connect for a local app (with --backend saucelabs)"
1276
+ }, {
1277
+ name: "expAccessKey",
1278
+ answer: "expEnvAccessKey",
1279
+ type: "string",
1280
+ desc: "Experitest access key (with --backend experitest)"
1281
+ }, {
1282
+ name: "expHostname",
1283
+ answer: "expEnvHostname",
1284
+ type: "string",
1285
+ desc: "Experitest cloud host (with --backend experitest)"
1286
+ }, {
1287
+ name: "expPort",
1288
+ answer: "expEnvPort",
1289
+ type: "string",
1290
+ desc: "Experitest cloud port (with --backend experitest)"
1291
+ }, {
1292
+ name: "expProtocol",
1293
+ answer: "expEnvProtocol",
1294
+ type: "string",
1295
+ desc: "Experitest protocol (with --backend experitest and a port other than 80 or 443)",
1296
+ choices: () => ({ http: "http", https: "https" })
1297
+ }, {
1298
+ name: "framework",
1299
+ answer: "framework",
1300
+ type: "string",
1301
+ desc: "Test framework",
1302
+ choices: packageChoices(() => SUPPORTED_PACKAGES.framework, (short, purpose, pkg) => pkg.startsWith("@serenity-js") ? `serenity-${purpose}` : short)
1303
+ }, {
1304
+ name: "typescript",
1305
+ answer: "isUsingTypeScript",
1306
+ type: "boolean",
1307
+ desc: "Write tests in TypeScript"
1308
+ }, {
1309
+ name: "generateTestFiles",
1310
+ answer: "generateTestFiles",
1311
+ type: "boolean",
1312
+ desc: "Generate example test files"
1313
+ }, {
1314
+ name: "specs",
1315
+ answer: "specs",
1316
+ type: "string",
1317
+ desc: "Location of spec or feature files (with --generate-test-files)"
1318
+ }, {
1319
+ name: "stepDefinitions",
1320
+ answer: "stepDefinitions",
1321
+ type: "string",
1322
+ desc: "Location of step definitions (with --framework cucumber)"
1323
+ }, {
1324
+ name: "pageObjects",
1325
+ answer: "usePageObjects",
1326
+ type: "boolean",
1327
+ desc: "Generate page objects (with --runner e2e and --generate-test-files)"
1328
+ }, {
1329
+ name: "pages",
1330
+ answer: "pages",
1331
+ type: "string",
1332
+ desc: "Location of page objects (with --page-objects)"
1333
+ }, {
1334
+ name: "serenityLibPath",
1335
+ answer: "serenityLibPath",
1336
+ type: "string",
1337
+ desc: "Location of the Serenity/JS Screenplay library (with --framework serenity-*)"
1338
+ }, {
1339
+ name: "agentSupport",
1340
+ answer: "agentSupport",
1341
+ type: "boolean",
1342
+ desc: "Write the AGENTS.md section and the wdio-session skill"
1343
+ }, {
1344
+ name: "reporters",
1345
+ answer: "reporters",
1346
+ type: "array",
1347
+ desc: "Comma-separated reporters",
1348
+ choices: packageChoices(() => SUPPORTED_PACKAGES.reporter)
1349
+ }, {
1350
+ name: "plugins",
1351
+ answer: "plugins",
1352
+ type: "array",
1353
+ desc: "Comma-separated plugins",
1354
+ choices: packageChoices(() => SUPPORTED_PACKAGES.plugin)
1355
+ }, {
1356
+ name: "visualTesting",
1357
+ answer: "includeVisualTesting",
1358
+ type: "boolean",
1359
+ desc: "Add visual testing (with --runner e2e or component)"
1360
+ }, {
1361
+ name: "services",
1362
+ answer: "services",
1363
+ type: "array",
1364
+ desc: "Comma-separated services, replaces the ones the wizard picks for your setup",
1365
+ choices: packageChoices(() => SUPPORTED_PACKAGES.service)
1366
+ }, {
1367
+ name: "npmInstall",
1368
+ answer: "npmInstall",
1369
+ type: "boolean",
1370
+ desc: "Install the dependencies"
1371
+ }];
1372
+ function toKebabCase(name) {
1373
+ return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
1374
+ }
1375
+ function flagFor(answer) {
1376
+ return ANSWER_FLAGS.find((flag) => flag.answer === answer);
1377
+ }
1378
+ var ANSWER_FLAGS_HELP = [
1379
+ "Every wizard question has a flag. A flag answers its question and the wizard",
1380
+ "asks the rest, or uses the defaults for the rest with --yes. Boolean flags",
1381
+ "take a --no- prefix, e.g. --no-typescript. A flag for a question the wizard",
1382
+ "does not ask for your setup is an error, and so is a value it does not offer."
1383
+ ].join("\n");
1384
+ function isAsked(question, answers) {
1385
+ const when = question.when;
1386
+ if (typeof when === "function") {
1387
+ return Boolean(when(answers));
1388
+ }
1389
+ return typeof when === "undefined" || Boolean(when);
1390
+ }
1391
+ function choiceValues(question, answers) {
1392
+ if (!question.choices) {
1393
+ return void 0;
1394
+ }
1395
+ const choices = typeof question.choices === "function" ? question.choices(answers) : question.choices;
1396
+ return choices.filter((choice) => typeof choice !== "undefined").map((choice) => choice && typeof choice === "object" ? choice.value : choice);
1397
+ }
1398
+ function assertAnswerFlagsApply(flagAnswers, answers) {
1399
+ for (const [answer, value] of Object.entries(flagAnswers)) {
1400
+ const flag = flagFor(answer);
1401
+ const usage = `--${toKebabCase(flag.name)}`;
1402
+ const questions = QUESTIONNAIRE.filter((question) => question.name === answer && isAsked(question, answers));
1403
+ if (questions.length === 0) {
1404
+ throw new AnswerFlagError(`${usage} does not apply to this setup. ${flag.desc}.`);
1405
+ }
1406
+ const offered = questions.map((question) => choiceValues(question, answers));
1407
+ if (offered.some((values) => typeof values === "undefined")) {
1408
+ continue;
1409
+ }
1410
+ const allowed = new Set(offered.flat());
1411
+ const rejected = [value].flat().filter((item) => !allowed.has(item));
1412
+ if (rejected.length === 0) {
1413
+ continue;
1414
+ }
1415
+ const cliValues = Object.entries(flag.choices());
1416
+ const toCli = (item) => cliValues.find(([, v]) => v === item)?.[0] ?? String(item);
1417
+ throw new AnswerFlagError(
1418
+ `${usage} ${rejected.map(toCli).join(",")} is not available for this setup. Use one of: ${[...new Set([...allowed].map(toCli))].join(", ")}`
1419
+ );
1420
+ }
1421
+ }
1422
+
1114
1423
  // src/utils.ts
1115
1424
  var NPM_COMMAND = /^win/.test(process.platform) ? "npm.cmd" : "npm";
1116
1425
  var __dirname = path2.dirname(url.fileURLToPath(import.meta.url));
1117
1426
  process.on("SIGINT", () => printAndExit(void 0, "SIGINT"));
1118
1427
  var TEMPLATE_ROOT_DIR = process.env.WDIO_UNIT_TESTS ? path2.join(__dirname, "templates", "exampleFiles") : path2.join(__dirname, "..", "templates", "exampleFiles");
1119
- function runProgram(command2, args, options) {
1428
+ function runProgram(command2, args, options, passUsageError = false) {
1120
1429
  const child = spawn(command2, args, { stdio: "inherit", ...options });
1121
1430
  return new Promise((resolve, rejects) => {
1122
1431
  let error;
1123
1432
  child.on("error", (e) => error = e);
1124
1433
  child.on("close", (code, signal) => {
1434
+ if (passUsageError && code === 2) {
1435
+ process.exit(2);
1436
+ return rejects(`Usage error calling: ${command2} ${args.join(" ")}`);
1437
+ }
1125
1438
  if (code !== 0) {
1126
1439
  const errorMessage = error && error.message || `Error calling: ${command2} ${args.join(" ")}`;
1127
1440
  printAndExit(errorMessage, signal);
@@ -1145,24 +1458,35 @@ function convertPackageHashToObject(pkg, hash = "$--$") {
1145
1458
  const [p, short, purpose] = pkg.split(hash);
1146
1459
  return { package: p, short, purpose };
1147
1460
  }
1148
- async function getAnswers(yes) {
1149
- if (yes) {
1150
- const ignoredQuestions = ["e2eEnvironment"];
1151
- const filteredQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name));
1152
- const answers2 = {};
1153
- for (const question of filteredQuestionaire) {
1154
- if (question.when && !question.when(answers2)) {
1155
- continue;
1156
- }
1157
- Object.assign(answers2, {
1158
- [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] : {}
1159
- });
1461
+ async function getAnswers(yes, flagAnswers = {}) {
1462
+ const answers = yes ? await getDefaultAnswers(flagAnswers) : await promptAnswers(flagAnswers);
1463
+ assertAnswerFlagsApply(flagAnswers, answers);
1464
+ return answers;
1465
+ }
1466
+ async function getDefaultAnswers(flagAnswers) {
1467
+ const ignoredQuestions = ["e2eEnvironment"];
1468
+ const filteredQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name));
1469
+ const answers = { ...flagAnswers };
1470
+ for (const question of filteredQuestionaire) {
1471
+ if (Object.prototype.hasOwnProperty.call(flagAnswers, question.name)) {
1472
+ continue;
1473
+ }
1474
+ if (question.when && !question.when(answers)) {
1475
+ continue;
1160
1476
  }
1161
- answers2.isUsingTypeScript = await answers2.isUsingTypeScript;
1162
- answers2.specs = await answers2.specs;
1163
- answers2.pages = await answers2.pages;
1164
- return answers2;
1477
+ if (typeof question.default === "undefined" && !question.choices) {
1478
+ continue;
1479
+ }
1480
+ Object.assign(answers, {
1481
+ [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] : {}
1482
+ });
1165
1483
  }
1484
+ answers.isUsingTypeScript = await answers.isUsingTypeScript;
1485
+ answers.specs = await answers.specs;
1486
+ answers.pages = await answers.pages;
1487
+ return answers;
1488
+ }
1489
+ async function promptAnswers(flagAnswers) {
1166
1490
  const projectProps = await getProjectProps(process.cwd());
1167
1491
  const isProjectExisting = Boolean(projectProps);
1168
1492
  const nameInPackageJsonIsNotCreateWdioDefault = projectProps?.packageJson?.name !== "my-new-project";
@@ -1198,7 +1522,7 @@ async function getAnswers(yes) {
1198
1522
  if (answers.createPackageJSON) {
1199
1523
  answers.projectRoot = process.cwd();
1200
1524
  }
1201
- return inquirer.prompt(QUESTIONNAIRE, answers);
1525
+ return inquirer.prompt(QUESTIONNAIRE, { ...answers, ...flagAnswers });
1202
1526
  }
1203
1527
  async function getProjectProps(cwd = process.cwd()) {
1204
1528
  try {
@@ -1254,11 +1578,8 @@ async function createPackageJSON(parsedAnswers) {
1254
1578
  return;
1255
1579
  }
1256
1580
  if (parsedAnswers.createPackageJSON === false) {
1257
- if (!packageJsonExists) {
1258
- console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`);
1259
- return !process.env.WDIO_UNIT_TESTS && process.exit(0);
1260
- }
1261
- return;
1581
+ console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`);
1582
+ process.exit(0);
1262
1583
  }
1263
1584
  if (parsedAnswers.createPackageJSON) {
1264
1585
  console.log(`Creating a ${chalk2.bold("package.json")} for the directory...`);
@@ -1305,10 +1626,13 @@ async function setupTypeScript(parsedAnswers) {
1305
1626
  const config = {
1306
1627
  compilerOptions: {
1307
1628
  // compiler
1308
- moduleResolution: "node",
1309
- module: !parsedAnswers.esmSupport ? "commonjs" : "ESNext",
1310
- target: "es2022",
1311
- lib: ["es2022", "dom"],
1629
+ // TypeScript 6 deprecates `moduleResolution: 'node'` (node10). ESM uses
1630
+ // `bundler`, which resolves imports as `tsx` does; CommonJS uses `NodeNext`,
1631
+ // which follows the `type` of the project's package.json.
1632
+ moduleResolution: parsedAnswers.esmSupport ? "bundler" : "NodeNext",
1633
+ module: parsedAnswers.esmSupport ? "ESNext" : "NodeNext",
1634
+ target: "es2024",
1635
+ lib: ["es2024", "dom"],
1312
1636
  types,
1313
1637
  skipLibCheck: true,
1314
1638
  // bundler
@@ -1453,8 +1777,10 @@ function getPreset(parsedAnswers) {
1453
1777
  function addServiceDeps(names, packages, update = false) {
1454
1778
  if (names.some(({ short }) => short === "appium")) {
1455
1779
  const result = execSync("appium --version || echo APPIUM_MISSING", { stdio: "pipe" }).toString().trim();
1456
- if (result === "APPIUM_MISSING") {
1457
- packages.push("appium");
1780
+ const major = Number.parseInt(result.replace(/^v/i, "").split(".")[0] || "", 10);
1781
+ const hasAppium3 = result !== "APPIUM_MISSING" && !Number.isNaN(major) && major >= 3;
1782
+ if (!hasAppium3) {
1783
+ packages.push("appium@^3");
1458
1784
  } else if (update) {
1459
1785
  console.log(
1460
1786
  "\n=======",
@@ -1514,10 +1840,15 @@ async function createWDIOScript(parsedAnswers) {
1514
1840
  console.log(chalk2.green(chalk2.bold("\u2714 Success!")));
1515
1841
  return true;
1516
1842
  }
1517
- async function runAppiumInstaller(parsedAnswers) {
1843
+ async function runAppiumInstaller(parsedAnswers, yes = false) {
1518
1844
  if (parsedAnswers.e2eEnvironment !== "mobile") {
1519
1845
  return;
1520
1846
  }
1847
+ if (yes || !process.stdin.isTTY) {
1848
+ return console.log(
1849
+ "Skipping the interactive Appium installer. Run `npx appium-installer` later, or see https://appium.io/docs/en/latest/quickstart/"
1850
+ );
1851
+ }
1521
1852
  const answer = await inquirer.prompt({
1522
1853
  name: "continueWithAppiumSetup",
1523
1854
  message: "Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?",
@@ -1613,6 +1944,10 @@ async function generateTestFiles(answers) {
1613
1944
  }
1614
1945
  return generateBrowserRunnerTestFiles(answers);
1615
1946
  }
1947
+ async function readDir(dir) {
1948
+ const entries = await fs2.readdir(dir, { recursive: true, withFileTypes: true });
1949
+ return entries.filter((entry) => entry.isFile()).map((entry) => path2.join(entry.parentPath, entry.name));
1950
+ }
1616
1951
  async function generateSerenityExamples(answers) {
1617
1952
  const templateDirectories = Object.entries({
1618
1953
  [answers.destSpecRootPath]: path2.join(TEMPLATE_ROOT_DIR, "serenity-js", answers.serenityAdapter),
@@ -1637,10 +1972,7 @@ async function generateLocalRunnerTestFiles(answers) {
1637
1972
  if (answers.usePageObjects) {
1638
1973
  testFiles.push(path2.join(TEMPLATE_ROOT_DIR, "pageobjects"));
1639
1974
  }
1640
- const files = (await Promise.all(testFiles.map((dirPath) => readDir(
1641
- dirPath,
1642
- [(file, stats) => !stats.isDirectory() && !(file.endsWith(".ejs") || file.endsWith(".feature"))]
1643
- )))).reduce((cur, acc) => [...acc, ...cur], []);
1975
+ const files = (await Promise.all(testFiles.map(readDir))).flat().filter((file) => file.endsWith(".ejs") || file.endsWith(".feature"));
1644
1976
  await Promise.all(files.map(async (file) => {
1645
1977
  const renderedTpl = await renderFile(file, { answers });
1646
1978
  const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset);
@@ -1710,10 +2042,79 @@ function replaceConfig(config, type, name) {
1710
2042
  import fss from "node:fs";
1711
2043
 
1712
2044
  // src/cli/utils.ts
1713
- import path3 from "node:path";
1714
- import fs3 from "node:fs/promises";
2045
+ import path4 from "node:path";
2046
+ import fs4 from "node:fs/promises";
1715
2047
  import inquirer2 from "inquirer";
1716
- async function runConfigCommand(parsedAnswers, npmTag) {
2048
+
2049
+ // src/cli/agent.ts
2050
+ import fs3 from "node:fs";
2051
+ import path3 from "node:path";
2052
+ import { fileURLToPath } from "node:url";
2053
+ var AGENT_SECTION_HEADING = "## End-to-end tests (WebdriverIO v10)";
2054
+ var AGENT_SECTION = `${AGENT_SECTION_HEADING}
2055
+
2056
+ - 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.
2057
+ - Config: \`wdio.conf.ts\`. Specs: \`test/specs/**/*.e2e.ts\`. Page objects: \`test/pageobjects/\`.
2058
+ - Run all tests: \`npx wdio run wdio.conf.ts\`
2059
+ - Run a single spec: \`npx wdio run wdio.conf.ts --spec test/specs/login.e2e.ts\`
2060
+ - Tests are async: always \`await\` commands, e.g. \`await $('button').click()\`. Never use the removed sync mode.
2061
+ - Prefer user-facing selectors: accessibility name or text (\`$('aria/Submit')\`, \`$('button=Submit')\`), then \`data-testid\`. Avoid XPath and generated CSS classes.
2062
+ - Rely on auto-waiting and \`expect-webdriverio\` matchers (\`await expect($('h1')).toHaveText('Welcome')\`) instead of \`browser.pause()\`.
2063
+ - To explore the app or verify a selector, use the \`wdio-mcp\` MCP server.
2064
+ - To drive the app from the shell, follow \`.agents/skills/wdio-session/SKILL.md\` (\`npx wdio session\`).
2065
+ - When a test fails, read the DevTools trace in \`test-results/\` (see \`transcript.md\`) before changing code.
2066
+ `;
2067
+ function readIfExists(file) {
2068
+ return fs3.existsSync(file) ? fs3.readFileSync(file, "utf-8") : void 0;
2069
+ }
2070
+ function readSessionSkill(start = path3.dirname(fileURLToPath(import.meta.url))) {
2071
+ let dir = start;
2072
+ for (let i = 0; i < 8; i++) {
2073
+ const candidates = [
2074
+ path3.join(dir, "wdio-session", "src", "skill", "SKILL.md"),
2075
+ path3.join(dir, "packages", "wdio-session", "src", "skill", "SKILL.md"),
2076
+ path3.join(dir, "node_modules", "@wdio", "session", "src", "skill", "SKILL.md"),
2077
+ path3.join(dir, "templates", "wdio-session", "SKILL.md")
2078
+ ];
2079
+ for (const candidate of candidates) {
2080
+ const markdown = readIfExists(candidate);
2081
+ if (markdown) {
2082
+ return markdown;
2083
+ }
2084
+ }
2085
+ const parent = path3.dirname(dir);
2086
+ if (parent === dir) {
2087
+ break;
2088
+ }
2089
+ dir = parent;
2090
+ }
2091
+ throw new Error("Cannot find the wdio-session skill (src/skill/SKILL.md).");
2092
+ }
2093
+ function writeAgentSupport(root) {
2094
+ const skillDest = path3.join(root, ".agents", "skills", "wdio-session", "SKILL.md");
2095
+ if (!fs3.existsSync(skillDest)) {
2096
+ const markdown = readSessionSkill();
2097
+ fs3.mkdirSync(path3.dirname(skillDest), { recursive: true });
2098
+ fs3.writeFileSync(skillDest, markdown);
2099
+ }
2100
+ const agentsPath = path3.join(root, "AGENTS.md");
2101
+ const agents = readIfExists(agentsPath) || "";
2102
+ if (!agents.includes(AGENT_SECTION_HEADING)) {
2103
+ const sep = agents.length === 0 ? "" : agents.endsWith("\n") ? "\n" : "\n\n";
2104
+ fs3.writeFileSync(agentsPath, agents + sep + AGENT_SECTION);
2105
+ }
2106
+ const gitignorePath = path3.join(root, ".gitignore");
2107
+ const gitignore = readIfExists(gitignorePath) || "";
2108
+ const hasEntry = gitignore.split("\n").some((line) => line.trim() === ".wdio/session/" || line.trim() === ".wdio/session");
2109
+ if (!hasEntry) {
2110
+ const sep = gitignore.length === 0 || gitignore.endsWith("\n") ? "" : "\n";
2111
+ fs3.writeFileSync(gitignorePath, `${gitignore}${sep}.wdio/session/
2112
+ `);
2113
+ }
2114
+ }
2115
+
2116
+ // src/cli/utils.ts
2117
+ async function runConfigCommand(parsedAnswers, npmTag, yes = false) {
1717
2118
  console.log("\n");
1718
2119
  await createPackageJSON(parsedAnswers);
1719
2120
  await setupTypeScript(parsedAnswers);
@@ -1742,10 +2143,13 @@ async function runConfigCommand(parsedAnswers, npmTag) {
1742
2143
  extraInfo: extraInfoParts.join("\n")
1743
2144
  })
1744
2145
  );
1745
- await runAppiumInstaller(parsedAnswers);
2146
+ await runAppiumInstaller(parsedAnswers, yes);
2147
+ if (parsedAnswers.rawAnswers?.agentSupport) {
2148
+ writeAgentSupport(parsedAnswers.projectRootDir);
2149
+ }
1746
2150
  }
1747
2151
  async function missingConfigurationPrompt(command2, configPath, runConfigCmd = runConfigCommand) {
1748
- 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?`;
2152
+ 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?`;
1749
2153
  const { config } = await inquirer2.prompt({
1750
2154
  type: "confirm",
1751
2155
  name: "config",
@@ -1754,10 +2158,7 @@ async function missingConfigurationPrompt(command2, configPath, runConfigCmd = r
1754
2158
  });
1755
2159
  if (!config) {
1756
2160
  console.log(`No WebdriverIO configuration found in "${process.cwd()}"`);
1757
- if (!process.env.WDIO_UNIT_TESTS) {
1758
- process.exit(0);
1759
- }
1760
- return configPath;
2161
+ process.exit(0);
1761
2162
  }
1762
2163
  const parsedAnswers = await parseAnswers(false);
1763
2164
  await runConfigCmd(parsedAnswers, "latest");
@@ -1769,8 +2170,8 @@ function resolveTsConfigFilePath({
1769
2170
  specs,
1770
2171
  runnerShort
1771
2172
  }) {
1772
- const rootTsConfigPath = path3.resolve(projectRootDir, "tsconfig.json");
1773
- const dedicatedTsConfigPath = path3.resolve(
2173
+ const rootTsConfigPath = path4.resolve(projectRootDir, "tsconfig.json");
2174
+ const dedicatedTsConfigPath = path4.resolve(
1774
2175
  projectRootDir,
1775
2176
  `tsconfig.${runnerShort === "local" ? "e2e" : "wdio"}.json`
1776
2177
  );
@@ -1780,13 +2181,13 @@ function resolveTsConfigFilePath({
1780
2181
  if (!specs) {
1781
2182
  return dedicatedTsConfigPath;
1782
2183
  }
1783
- const specPathWithoutGlobs = specs.split(path3.sep).filter((segment) => !segment.includes("*")).join(path3.sep);
1784
- const candidate = path3.resolve(projectRootDir, path3.dirname(specPathWithoutGlobs), "tsconfig.json");
1785
- return path3.resolve(candidate) === rootTsConfigPath ? dedicatedTsConfigPath : candidate;
2184
+ const specPathWithoutGlobs = specs.split(path4.sep).filter((segment) => !segment.includes("*")).join(path4.sep);
2185
+ const candidate = path4.resolve(projectRootDir, path4.dirname(specPathWithoutGlobs), "tsconfig.json");
2186
+ return path4.resolve(candidate) === rootTsConfigPath ? dedicatedTsConfigPath : candidate;
1786
2187
  }
1787
- var parseAnswers = async function(yes) {
2188
+ var parseAnswers = async function(yes, flagAnswers = {}) {
1788
2189
  console.log(CONFIG_HELPER_INTRO);
1789
- const answers = await getAnswers(yes);
2190
+ const answers = await getAnswers(yes, flagAnswers);
1790
2191
  const frameworkPackage = convertPackageHashToObject(answers.framework);
1791
2192
  const runnerPackage = convertPackageHashToObject(answers.runner || SUPPORTED_PACKAGES.runner[0].value);
1792
2193
  const servicePackages = answers.services.map((service) => convertPackageHashToObject(service));
@@ -1805,7 +2206,7 @@ var parseAnswers = async function(yes) {
1805
2206
  ...servicePackages.map((service) => service.package),
1806
2207
  ...serenityPackages
1807
2208
  ].filter(Boolean);
1808
- const hasRootTSConfig = await fs3.access(path3.resolve(projectRootDir, "tsconfig.json")).then(() => true, () => false);
2209
+ const hasRootTSConfig = await fs4.access(path4.resolve(projectRootDir, "tsconfig.json")).then(() => true, () => false);
1809
2210
  const tsConfigFilePath = resolveTsConfigFilePath({
1810
2211
  projectRootDir,
1811
2212
  hasRootTSConfig,
@@ -1815,7 +2216,7 @@ var parseAnswers = async function(yes) {
1815
2216
  const parsedPaths = getPathForFileGeneration(answers, projectRootDir);
1816
2217
  const isUsingTypeScript = answers.isUsingTypeScript;
1817
2218
  const wdioConfigFilename = `wdio.conf.${isUsingTypeScript ? "ts" : "js"}`;
1818
- const wdioConfigPath = path3.resolve(projectRootDir, wdioConfigFilename);
2219
+ const wdioConfigPath = path4.resolve(projectRootDir, wdioConfigFilename);
1819
2220
  return {
1820
2221
  projectName: projectProps?.packageJson.name || "Test Suite",
1821
2222
  // default values required in templates
@@ -1834,8 +2235,8 @@ var parseAnswers = async function(yes) {
1834
2235
  reporters: reporterPackages.map(({ short }) => short),
1835
2236
  plugins: pluginPackages.map(({ short }) => short),
1836
2237
  services: servicePackages.map(({ short }) => short),
1837
- specs: answers.specs && `./${path3.relative(projectRootDir, answers.specs).replaceAll(path3.sep, "/")}`,
1838
- stepDefinitions: answers.stepDefinitions && `./${path3.relative(projectRootDir, answers.stepDefinitions).replaceAll(path3.sep, "/")}`,
2238
+ specs: answers.specs && `./${path4.relative(projectRootDir, answers.specs).replaceAll(path4.sep, "/")}`,
2239
+ stepDefinitions: answers.stepDefinitions && `./${path4.relative(projectRootDir, answers.stepDefinitions).replaceAll(path4.sep, "/")}`,
1839
2240
  packagesToInstall,
1840
2241
  isUsingTypeScript,
1841
2242
  esmSupport: projectProps && !projectProps.esmSupported ? false : true,
@@ -1850,20 +2251,20 @@ var parseAnswers = async function(yes) {
1850
2251
  relativePath: parsedPaths.relativePath,
1851
2252
  hasRootTSConfig,
1852
2253
  tsConfigFilePath,
1853
- tsProject: `./${path3.relative(projectRootDir, tsConfigFilePath).replaceAll(path3.sep, "/")}`,
2254
+ tsProject: `./${path4.relative(projectRootDir, tsConfigFilePath).replaceAll(path4.sep, "/")}`,
1854
2255
  wdioConfigPath
1855
2256
  };
1856
2257
  };
1857
2258
  async function canAccessConfigPath(configPathNoExtension, configPath) {
1858
2259
  return new Promise((resolve, reject) => {
1859
2260
  if (configPath) {
1860
- fs3.access(configPath).then(() => resolve(configPath), reject);
2261
+ fs4.access(configPath).then(() => resolve(configPath), reject);
1861
2262
  } else {
1862
2263
  reject();
1863
2264
  }
1864
2265
  }).catch(() => Promise.all(SUPPORTED_CONFIG_FILE_EXTENSION.map(async (supportedExtension) => {
1865
2266
  const configPathWithExtension = `${configPathNoExtension}.${supportedExtension}`;
1866
- return fs3.access(configPathWithExtension).then(() => configPathWithExtension, () => void 0);
2267
+ return fs4.access(configPathWithExtension).then(() => configPathWithExtension, () => void 0);
1867
2268
  })).then(
1868
2269
  (configFilePaths) => configFilePaths.find(Boolean),
1869
2270
  () => void 0
@@ -1929,7 +2330,7 @@ Available options for a ${type} are:
1929
2330
  }
1930
2331
  process.exit(0);
1931
2332
  }
1932
- const defaultPath = path4.resolve(process.cwd(), "wdio.conf");
2333
+ const defaultPath = path5.resolve(process.cwd(), "wdio.conf");
1933
2334
  const wdioConfPathWithNoExtension = config ? (await formatConfigFilePaths(config)).fullPathNoExtension : defaultPath;
1934
2335
  const wdioConfPath = await canAccessConfigPath(wdioConfPathWithNoExtension);
1935
2336
  if (!wdioConfPath) {
@@ -1940,7 +2341,7 @@ Available options for a ${type} are:
1940
2341
  process.exit(1);
1941
2342
  }
1942
2343
  }
1943
- const configFile = await fs4.readFile(wdioConfPath, { encoding: "utf-8" });
2344
+ const configFile = await fs5.readFile(wdioConfPath, { encoding: "utf-8" });
1944
2345
  const match = findInConfig(configFile, type);
1945
2346
  const projectRoot = await getProjectRoot();
1946
2347
  if (match && match[0].includes(name)) {
@@ -1959,9 +2360,9 @@ Available options for a ${type} are:
1959
2360
  console.log(`Package "${selectedPackage.package}" installed successfully.`);
1960
2361
  const newConfig = replaceConfig(configFile, type, name);
1961
2362
  if (!newConfig) {
1962
- throw new Error(`Couldn't find "${type}" property in ${path4.basename(wdioConfPath)}`);
2363
+ throw new Error(`Couldn't find "${type}" property in ${path5.basename(wdioConfPath)}`);
1963
2364
  }
1964
- await fs4.writeFile(wdioConfPath, newConfig, { encoding: "utf-8" });
2365
+ await fs5.writeFile(wdioConfPath, newConfig, { encoding: "utf-8" });
1965
2366
  console.log("Your wdio.conf.js file has been updated.");
1966
2367
  process.exit(0);
1967
2368
  }