@wdio/cli 9.0.0-alpha.9 → 9.0.0

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.
Files changed (86) hide show
  1. package/build/commands/config.d.ts.map +1 -1
  2. package/build/commands/repl.d.ts.map +1 -1
  3. package/build/commands/run.d.ts +14 -13
  4. package/build/commands/run.d.ts.map +1 -1
  5. package/build/constants.d.ts +83 -10
  6. package/build/constants.d.ts.map +1 -1
  7. package/build/index.cjs +46 -0
  8. package/build/index.d.cts +2 -0
  9. package/build/index.d.cts.map +1 -0
  10. package/build/index.js +3233 -4
  11. package/build/interface.d.ts +0 -1
  12. package/build/interface.d.ts.map +1 -1
  13. package/build/launcher.d.ts.map +1 -1
  14. package/build/run.d.ts.map +1 -1
  15. package/build/types.d.ts +3 -4
  16. package/build/types.d.ts.map +1 -1
  17. package/build/utils.d.ts +7 -11
  18. package/build/utils.d.ts.map +1 -1
  19. package/build/watcher.d.ts.map +1 -1
  20. package/package.json +20 -25
  21. package/build/cjs/index.d.ts +0 -2
  22. package/build/cjs/index.d.ts.map +0 -1
  23. package/build/cjs/index.js +0 -26
  24. package/build/cjs/package.json +0 -5
  25. package/build/commands/config.js +0 -197
  26. package/build/commands/index.js +0 -5
  27. package/build/commands/install.js +0 -109
  28. package/build/commands/repl.js +0 -50
  29. package/build/commands/run.js +0 -262
  30. package/build/constants.js +0 -909
  31. package/build/install.js +0 -38
  32. package/build/interface.js +0 -285
  33. package/build/launcher.js +0 -513
  34. package/build/run.js +0 -75
  35. package/build/templates/EjsHelpers.js +0 -59
  36. package/build/templates/EjsHelpers.ts +0 -84
  37. package/build/templates/exampleFiles/browser/Component.css.ejs +0 -121
  38. package/build/templates/exampleFiles/browser/Component.lit.ejs +0 -154
  39. package/build/templates/exampleFiles/browser/Component.lit.test.ejs +0 -24
  40. package/build/templates/exampleFiles/browser/Component.preact.ejs +0 -28
  41. package/build/templates/exampleFiles/browser/Component.preact.test.ejs +0 -59
  42. package/build/templates/exampleFiles/browser/Component.react.ejs +0 -29
  43. package/build/templates/exampleFiles/browser/Component.react.test.ejs +0 -58
  44. package/build/templates/exampleFiles/browser/Component.solid.ejs +0 -28
  45. package/build/templates/exampleFiles/browser/Component.solid.test.ejs +0 -58
  46. package/build/templates/exampleFiles/browser/Component.stencil.ejs +0 -43
  47. package/build/templates/exampleFiles/browser/Component.stencil.test.ejs +0 -45
  48. package/build/templates/exampleFiles/browser/Component.svelte.ejs +0 -47
  49. package/build/templates/exampleFiles/browser/Component.svelte.test.ejs +0 -58
  50. package/build/templates/exampleFiles/browser/Component.vue.ejs +0 -34
  51. package/build/templates/exampleFiles/browser/Component.vue.test.ejs +0 -62
  52. package/build/templates/exampleFiles/browser/standalone.test.ejs +0 -13
  53. package/build/templates/exampleFiles/cucumber/features/login.feature +0 -12
  54. package/build/templates/exampleFiles/cucumber/step_definitions/steps.js.ejs +0 -55
  55. package/build/templates/exampleFiles/mochaJasmine/test.e2e.js.ejs +0 -11
  56. package/build/templates/exampleFiles/pageobjects/login.page.js.ejs +0 -45
  57. package/build/templates/exampleFiles/pageobjects/page.js.ejs +0 -17
  58. package/build/templates/exampleFiles/pageobjects/secure.page.js.ejs +0 -20
  59. package/build/templates/exampleFiles/serenity-js/common/config/serenity.properties.ejs +0 -1
  60. package/build/templates/exampleFiles/serenity-js/common/serenity/github-api/GitHubStatus.ts.ejs +0 -41
  61. package/build/templates/exampleFiles/serenity-js/common/serenity/todo-list-app/TodoList.ts.ejs +0 -100
  62. package/build/templates/exampleFiles/serenity-js/common/serenity/todo-list-app/TodoListItem.ts.ejs +0 -36
  63. package/build/templates/exampleFiles/serenity-js/cucumber/step-definitions/steps.ts.ejs +0 -37
  64. package/build/templates/exampleFiles/serenity-js/cucumber/support/parameter.config.ts.ejs +0 -18
  65. package/build/templates/exampleFiles/serenity-js/cucumber/todo-list/completing_items.feature.ejs +0 -23
  66. package/build/templates/exampleFiles/serenity-js/cucumber/todo-list/narrative.md.ejs +0 -17
  67. package/build/templates/exampleFiles/serenity-js/jasmine/example.spec.ts.ejs +0 -86
  68. package/build/templates/exampleFiles/serenity-js/mocha/example.spec.ts.ejs +0 -88
  69. package/build/templates/snippets/afterTest.ejs +0 -20
  70. package/build/templates/snippets/capabilities.ejs +0 -57
  71. package/build/templates/snippets/cucumber.ejs +0 -50
  72. package/build/templates/snippets/electronTest.js.ejs +0 -7
  73. package/build/templates/snippets/jasmine.ejs +0 -20
  74. package/build/templates/snippets/macosTest.js.ejs +0 -11
  75. package/build/templates/snippets/mocha.ejs +0 -14
  76. package/build/templates/snippets/reporters.ejs +0 -14
  77. package/build/templates/snippets/serenity.ejs +0 -18
  78. package/build/templates/snippets/services.ejs +0 -18
  79. package/build/templates/snippets/testWithPO.js.ejs +0 -22
  80. package/build/templates/snippets/testWithoutPO.js.ejs +0 -19
  81. package/build/templates/snippets/vscodeTest.js.ejs +0 -9
  82. package/build/templates/wdio.conf.tpl.ejs +0 -422
  83. package/build/types.js +0 -1
  84. package/build/utils.js +0 -930
  85. package/build/watcher.js +0 -156
  86. /package/{LICENSE-MIT → LICENSE} +0 -0
package/build/utils.js DELETED
@@ -1,930 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import util, { promisify } from 'node:util';
3
- import path, { dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { execSync, spawn } from 'node:child_process';
6
- import ejs from 'ejs';
7
- import chalk from 'chalk';
8
- import inquirer from 'inquirer';
9
- import pickBy from 'lodash.pickby';
10
- import logger from '@wdio/logger';
11
- import readDir from 'recursive-readdir';
12
- import { $ } from 'execa';
13
- import { readPackageUp } from 'read-pkg-up';
14
- import { resolve } from 'import-meta-resolve';
15
- import { SevereServiceError } from 'webdriverio';
16
- import { ConfigParser } from '@wdio/config/node';
17
- import { CAPABILITY_KEYS } from '@wdio/protocols';
18
- import { installPackages, getInstallCommand } from './install.js';
19
- import { ANDROID_CONFIG, CompilerOptions, DEPENDENCIES_INSTALLATION_MESSAGE, IOS_CONFIG, pkg, QUESTIONNAIRE, TESTING_LIBRARY_PACKAGES, COMMUNITY_PACKAGES_WITH_TS_SUPPORT, usesSerenity, PMs, } from './constants.js';
20
- import { EjsHelpers } from './templates/EjsHelpers.js';
21
- const log = logger('@wdio/cli:utils');
22
- const __dirname = dirname(fileURLToPath(import.meta.url));
23
- const NPM_COMMAND = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';
24
- const VERSION_REGEXP = /(\d+)\.(\d+)\.(\d+)-(alpha|beta|)\.(\d+)\+(.+)/g;
25
- const TEMPLATE_ROOT_DIR = path.join(__dirname, 'templates', 'exampleFiles');
26
- export const renderFile = promisify(ejs.renderFile);
27
- export class HookError extends SevereServiceError {
28
- origin;
29
- constructor(message, origin) {
30
- super(message);
31
- this.origin = origin;
32
- }
33
- }
34
- /**
35
- * run service launch sequences
36
- */
37
- export async function runServiceHook(launcher, hookName, ...args) {
38
- const start = Date.now();
39
- return Promise.all(launcher.map(async (service) => {
40
- try {
41
- if (typeof service[hookName] === 'function') {
42
- await service[hookName](...args);
43
- }
44
- }
45
- catch (err) {
46
- const message = `A service failed in the '${hookName}' hook\n${err.stack}\n\n`;
47
- if (err instanceof SevereServiceError || err.name === 'SevereServiceError') {
48
- return { status: 'rejected', reason: message, origin: hookName };
49
- }
50
- log.error(`${message}Continue...`);
51
- }
52
- })).then(results => {
53
- if (launcher.length) {
54
- log.debug(`Finished to run "${hookName}" hook in ${Date.now() - start}ms`);
55
- }
56
- const rejectedHooks = results.filter(p => p && p.status === 'rejected');
57
- if (rejectedHooks.length) {
58
- return Promise.reject(new HookError(`\n${rejectedHooks.map(p => p && p.reason).join()}\n\nStopping runner...`, hookName));
59
- }
60
- });
61
- }
62
- /**
63
- * Run hook in service launcher
64
- * @param {Array|Function} hook - can be array of functions or single function
65
- * @param {object} config
66
- * @param {object} capabilities
67
- */
68
- export async function runLauncherHook(hook, ...args) {
69
- if (typeof hook === 'function') {
70
- hook = [hook];
71
- }
72
- const catchFn = (e) => {
73
- log.error(`Error in hook: ${e.stack}`);
74
- if (e instanceof SevereServiceError) {
75
- throw new HookError(e.message, hook[0].name);
76
- }
77
- };
78
- return Promise.all(hook.map((hook) => {
79
- try {
80
- return hook(...args);
81
- }
82
- catch (err) {
83
- return catchFn(err);
84
- }
85
- })).catch(catchFn);
86
- }
87
- /**
88
- * Run onCompleteHook in Launcher
89
- * @param {Array|Function} onCompleteHook - can be array of functions or single function
90
- * @param {*} config
91
- * @param {*} capabilities
92
- * @param {*} exitCode
93
- * @param {*} results
94
- */
95
- export async function runOnCompleteHook(onCompleteHook, config, capabilities, exitCode, results) {
96
- if (typeof onCompleteHook === 'function') {
97
- onCompleteHook = [onCompleteHook];
98
- }
99
- return Promise.all(onCompleteHook.map(async (hook) => {
100
- try {
101
- await hook(exitCode, config, capabilities, results);
102
- return 0;
103
- }
104
- catch (err) {
105
- log.error(`Error in onCompleteHook: ${err.stack}`);
106
- if (err instanceof SevereServiceError) {
107
- throw new HookError(err.message, 'onComplete');
108
- }
109
- return 1;
110
- }
111
- }));
112
- }
113
- /**
114
- * get runner identification by caps
115
- */
116
- export function getRunnerName(caps = {}) {
117
- let runner = caps.browserName ||
118
- caps.platformName ||
119
- caps['appium:platformName'] ||
120
- caps['appium:appPackage'] ||
121
- caps['appium:appWaitActivity'] ||
122
- caps['appium:app'];
123
- // MultiRemote
124
- if (!runner) {
125
- runner = Object.values(caps).length === 0 || Object.values(caps).some(cap => !cap.capabilities) ? 'undefined' : 'MultiRemote';
126
- }
127
- return runner;
128
- }
129
- function buildNewConfigArray(str, type, change) {
130
- const newStr = str
131
- .split(`${type}s: `)[1]
132
- .replace(/'/g, '');
133
- const newArray = newStr.match(/(\w*)/gmi)?.filter(e => !!e).concat([change]) || [];
134
- return str
135
- .replace('// ', '')
136
- .replace(new RegExp(`(${type}s: )((.*\\s*)*)`), `$1[${newArray.map(e => `'${e}'`)}]`);
137
- }
138
- function buildNewConfigString(str, type, change) {
139
- return str.replace(new RegExp(`(${type}: )('\\w*')`), `$1'${change}'`);
140
- }
141
- export function findInConfig(config, type) {
142
- let regexStr = `[\\/\\/]*[\\s]*${type}s: [\\s]*\\[([\\s]*['|"]\\w*['|"],*)*[\\s]*\\]`;
143
- if (type === 'framework') {
144
- regexStr = `[\\/\\/]*[\\s]*${type}: ([\\s]*['|"]\\w*['|"])`;
145
- }
146
- const regex = new RegExp(regexStr, 'gmi');
147
- return config.match(regex);
148
- }
149
- export function replaceConfig(config, type, name) {
150
- if (type === 'framework') {
151
- return buildNewConfigString(config, type, name);
152
- }
153
- const match = findInConfig(config, type);
154
- if (!match || match.length === 0) {
155
- return;
156
- }
157
- const text = match.pop() || '';
158
- return config.replace(text, buildNewConfigArray(text, type, name));
159
- }
160
- export function addServiceDeps(names, packages, update = false) {
161
- /**
162
- * install Appium if it is not installed globally if `@wdio/appium-service`
163
- * was selected for install
164
- */
165
- if (names.some(({ short }) => short === 'appium')) {
166
- const result = execSync('appium --version || echo APPIUM_MISSING', { stdio: 'pipe' }).toString().trim();
167
- if (result === 'APPIUM_MISSING') {
168
- packages.push('appium');
169
- }
170
- else if (update) {
171
- // eslint-disable-next-line no-console
172
- console.log('\n=======', '\nUsing globally installed appium', result, '\nPlease add the following to your wdio.conf.js:', "\nappium: { command: 'appium' }", '\n=======\n');
173
- }
174
- }
175
- }
176
- /**
177
- * @todo add JSComments
178
- */
179
- export function convertPackageHashToObject(pkg, hash = '$--$') {
180
- const [p, short, purpose] = pkg.split(hash);
181
- return { package: p, short, purpose };
182
- }
183
- export function getSerenityPackages(answers) {
184
- const framework = convertPackageHashToObject(answers.framework);
185
- if (framework.package !== '@serenity-js/webdriverio') {
186
- return [];
187
- }
188
- const isUsingTypeScript = answers.isUsingCompiler === CompilerOptions.TS;
189
- const packages = {
190
- cucumber: [
191
- '@cucumber/cucumber',
192
- '@serenity-js/cucumber',
193
- ],
194
- mocha: [
195
- '@serenity-js/mocha',
196
- 'mocha',
197
- isUsingTypeScript && '@types/mocha',
198
- ],
199
- jasmine: [
200
- '@serenity-js/jasmine',
201
- 'jasmine',
202
- isUsingTypeScript && '@types/jasmine',
203
- ],
204
- common: [
205
- '@serenity-js/assertions',
206
- '@serenity-js/console-reporter',
207
- '@serenity-js/core',
208
- '@serenity-js/rest',
209
- '@serenity-js/serenity-bdd',
210
- '@serenity-js/web',
211
- isUsingTypeScript && '@types/node',
212
- 'npm-failsafe',
213
- 'rimraf',
214
- ]
215
- };
216
- return [
217
- ...packages[framework.purpose],
218
- ...packages.common,
219
- ].filter(Boolean).sort();
220
- }
221
- export async function getCapabilities(arg) {
222
- const optionalCapabilites = {
223
- platformVersion: arg.platformVersion,
224
- udid: arg.udid,
225
- ...(arg.deviceName && { deviceName: arg.deviceName })
226
- };
227
- /**
228
- * Parsing of option property and constructing desiredCapabilities
229
- * for Appium session. Could be application(1) or browser(2-3) session.
230
- */
231
- if (/.*\.(apk|app|ipa)$/.test(arg.option)) {
232
- return {
233
- capabilities: {
234
- app: arg.option,
235
- ...(arg.option.endsWith('apk') ? ANDROID_CONFIG : IOS_CONFIG),
236
- ...optionalCapabilites,
237
- }
238
- };
239
- }
240
- else if (/android/.test(arg.option)) {
241
- return { capabilities: { browserName: 'Chrome', ...ANDROID_CONFIG, ...optionalCapabilites } };
242
- }
243
- else if (/ios/.test(arg.option)) {
244
- return { capabilities: { browserName: 'Safari', ...IOS_CONFIG, ...optionalCapabilites } };
245
- }
246
- else if (/(js|ts)$/.test(arg.option)) {
247
- const config = new ConfigParser(arg.option);
248
- try {
249
- await config.initialize();
250
- }
251
- catch (e) {
252
- throw Error(e.code === 'MODULE_NOT_FOUND' ? `Config File not found: ${arg.option}` :
253
- `Could not parse ${arg.option}, failed with error : ${e.message}`);
254
- }
255
- if (typeof arg.capabilities === 'undefined') {
256
- throw Error('Please provide index/named property of capability to use from the capabilities array/object in wdio config file');
257
- }
258
- let requiredCaps = config.getCapabilities();
259
- requiredCaps = (
260
- // multi capabilities
261
- requiredCaps[parseInt(arg.capabilities, 10)] ||
262
- // multiremote
263
- requiredCaps[arg.capabilities]);
264
- const requiredW3CCaps = pickBy(requiredCaps, (_, key) => CAPABILITY_KEYS.includes(key) || key.includes(':'));
265
- if (!Object.keys(requiredW3CCaps).length) {
266
- 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.`);
267
- }
268
- return { capabilities: { ...requiredW3CCaps } };
269
- }
270
- return { capabilities: { browserName: arg.option } };
271
- }
272
- /**
273
- * Checks if certain directory has babel configuration files
274
- * @param rootDir directory where this function checks for Babel signs
275
- * @returns true, if a babel config was found, otherwise false
276
- */
277
- export function hasBabelConfig(rootDir) {
278
- return Promise.all([
279
- fs.access(path.join(rootDir, 'babel.js')),
280
- fs.access(path.join(rootDir, 'babel.cjs')),
281
- fs.access(path.join(rootDir, 'babel.mjs')),
282
- fs.access(path.join(rootDir, '.babelrc'))
283
- ]).then((results) => results.filter(Boolean).length > 1, () => false);
284
- }
285
- /**
286
- * detect if project has a compiler file
287
- */
288
- export async function detectCompiler(answers) {
289
- const root = await getProjectRoot(answers);
290
- const rootTSConfigExist = await fs.access(path.resolve(root, 'tsconfig.json')).then(() => true, () => false);
291
- return (await hasBabelConfig(root))
292
- ? CompilerOptions.Babel // default to Babel
293
- : rootTSConfigExist
294
- ? CompilerOptions.TS // default to TypeScript
295
- : CompilerOptions.Nil; // default to no compiler
296
- }
297
- /**
298
- * Check if package is installed
299
- * @param {string} package to check existance for
300
- */
301
- export async function hasPackage(pkg) {
302
- try {
303
- await resolve(pkg, import.meta.url);
304
- return true;
305
- }
306
- catch (err) {
307
- return false;
308
- }
309
- }
310
- /**
311
- * generate test files based on CLI answers
312
- */
313
- export async function generateTestFiles(answers) {
314
- if (answers.serenityAdapter) {
315
- return generateSerenityExamples(answers);
316
- }
317
- if (answers.runner === 'local') {
318
- return generateLocalRunnerTestFiles(answers);
319
- }
320
- return generateBrowserRunnerTestFiles(answers);
321
- }
322
- const TSX_BASED_FRAMEWORKS = ['react', 'preact', 'solid', 'stencil'];
323
- export async function generateBrowserRunnerTestFiles(answers) {
324
- const isUsingFramework = typeof answers.preset === 'string';
325
- const preset = getPreset(answers);
326
- const tplRootDir = path.join(TEMPLATE_ROOT_DIR, 'browser');
327
- await fs.mkdir(answers.destSpecRootPath, { recursive: true });
328
- /**
329
- * render css file
330
- */
331
- if (isUsingFramework) {
332
- const renderedCss = await renderFile(path.join(tplRootDir, 'Component.css.ejs'), { answers });
333
- await fs.writeFile(path.join(answers.destSpecRootPath, 'Component.css'), renderedCss);
334
- }
335
- /**
336
- * render component file
337
- */
338
- const testExt = `${(answers.isUsingTypeScript ? 'ts' : 'js')}${TSX_BASED_FRAMEWORKS.includes(preset) ? 'x' : ''}`;
339
- const fileExt = ['svelte', 'vue'].includes(preset)
340
- ? preset
341
- : testExt;
342
- if (preset) {
343
- const componentOutFileName = `Component.${fileExt}`;
344
- const renderedComponent = await renderFile(path.join(tplRootDir, `Component.${preset}.ejs`), { answers });
345
- await fs.writeFile(path.join(answers.destSpecRootPath, componentOutFileName), renderedComponent);
346
- }
347
- /**
348
- * render test file
349
- */
350
- const componentFileName = preset ? `Component.${preset}.test.ejs` : 'standalone.test.ejs';
351
- const renderedTest = await renderFile(path.join(tplRootDir, componentFileName), { answers });
352
- await fs.writeFile(path.join(answers.destSpecRootPath, `Component.test.${testExt}`), renderedTest);
353
- }
354
- async function generateLocalRunnerTestFiles(answers) {
355
- const testFiles = answers.framework === 'cucumber'
356
- ? [path.join(TEMPLATE_ROOT_DIR, 'cucumber')]
357
- : [path.join(TEMPLATE_ROOT_DIR, 'mochaJasmine')];
358
- if (answers.usePageObjects) {
359
- testFiles.push(path.join(TEMPLATE_ROOT_DIR, 'pageobjects'));
360
- }
361
- const files = (await Promise.all(testFiles.map((dirPath) => readDir(dirPath, [(file, stats) => !stats.isDirectory() && !(file.endsWith('.ejs') || file.endsWith('.feature'))])))).reduce((cur, acc) => [...acc, ...(cur)], []);
362
- for (const file of files) {
363
- const renderedTpl = await renderFile(file, { answers });
364
- const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset);
365
- const fileEnding = (answers.isUsingTypeScript ? '.ts' : '.js') + (isJSX ? 'x' : '');
366
- const destPath = (file.endsWith('page.js.ejs')
367
- ? path.join(answers.destPageObjectRootPath, path.basename(file))
368
- : file.includes('step_definition')
369
- ? path.join(answers.destStepRootPath, path.basename(file))
370
- : path.join(answers.destSpecRootPath, path.basename(file))).replace(/\.ejs$/, '').replace(/\.js$/, fileEnding);
371
- await fs.mkdir(path.dirname(destPath), { recursive: true });
372
- await fs.writeFile(destPath, renderedTpl);
373
- }
374
- }
375
- async function generateSerenityExamples(answers) {
376
- const templateDirectories = {
377
- [answers.projectRootDir]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'config'),
378
- [answers.destSpecRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', answers.serenityAdapter),
379
- [answers.destSerenityLibRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'serenity'),
380
- };
381
- for (const [destinationRootDir, templateRootDir] of Object.entries(templateDirectories)) {
382
- const pathsToTemplates = await readDir(templateRootDir);
383
- for (const pathToTemplate of pathsToTemplates) {
384
- const extension = answers.isUsingTypeScript ? '.ts' : '.js';
385
- const destination = path.join(destinationRootDir, path.relative(templateRootDir, pathToTemplate))
386
- .replace(/\.ejs$/, '')
387
- .replace(/\.ts$/, extension);
388
- const contents = await renderFile(pathToTemplate, { answers, _: new EjsHelpers({ useEsm: answers.esmSupport, useTypeScript: answers.isUsingTypeScript }) });
389
- await fs.mkdir(path.dirname(destination), { recursive: true });
390
- await fs.writeFile(destination, contents);
391
- }
392
- }
393
- }
394
- export async function getAnswers(yes) {
395
- if (yes) {
396
- const ignoredQuestions = ['e2eEnvironment'];
397
- const filterdQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name));
398
- const answers = {};
399
- for (const question of filterdQuestionaire) {
400
- /**
401
- * set nothing if question doesn't apply
402
- */
403
- if (question.when && !question.when(answers)) {
404
- continue;
405
- }
406
- Object.assign(answers, {
407
- [question.name]: typeof question.default !== 'undefined'
408
- /**
409
- * set default value if existing
410
- */
411
- ? typeof question.default === 'function'
412
- ? await question.default(answers)
413
- : await question.default
414
- : question.choices && question.choices.length
415
- /**
416
- * pick first choice, select value if it exists
417
- */
418
- ? typeof question.choices === 'function'
419
- ? question.choices(answers)[0].value
420
- ? question.choices(answers)[0].value
421
- : question.choices(answers)[0]
422
- : question.choices[0].value
423
- ? question.choices[0].value
424
- : question.choices[0]
425
- : {}
426
- });
427
- }
428
- /**
429
- * some questions have async defaults
430
- */
431
- answers.isUsingCompiler = await answers.isUsingCompiler;
432
- answers.specs = await answers.specs;
433
- answers.pages = await answers.pages;
434
- return answers;
435
- }
436
- const projectProps = await getProjectProps(process.cwd());
437
- const isProjectExisting = Boolean(projectProps);
438
- const projectName = projectProps?.packageJson?.name ? ` named "${projectProps.packageJson.name}"` : '';
439
- const questions = [
440
- /**
441
- * in case the `wdio config` was called using a global installed @wdio/cli package
442
- */
443
- ...(!isProjectExisting
444
- ? [{
445
- type: 'confirm',
446
- name: 'createPackageJSON',
447
- default: true,
448
- message: `Couldn't find a package.json in "${process.cwd()}" or any of the parent directories, do you want to create one?`,
449
- }]
450
- /**
451
- * in case create-wdio was used which creates a package.json with name "my-new-project"
452
- * we don't need to ask this question
453
- */
454
- : projectProps?.packageJson?.name !== 'my-new-project'
455
- ? [{
456
- type: 'confirm',
457
- name: 'projectRootCorrect',
458
- default: true,
459
- message: `A project${projectName} was detected at "${projectProps?.path}", correct?`,
460
- }, {
461
- type: 'input',
462
- name: 'projectRoot',
463
- message: 'What is the project root for your test project?',
464
- default: projectProps?.path,
465
- // only ask if there are more than 1 runner to pick from
466
- when: /* istanbul ignore next */ (answers) => !answers.projectRootCorrect
467
- }]
468
- : []),
469
- ...QUESTIONNAIRE
470
- ];
471
- return inquirer.prompt(questions);
472
- }
473
- /**
474
- * Generates a valid file path from answers provided.
475
- * @param answers The answer from which a file path is to be generated.
476
- * @param projectRootDir The root directory of the project.
477
- * @returns filePath
478
- */
479
- function generatePathfromAnswer(answers, projectRootDir) {
480
- return path.resolve(projectRootDir, path.dirname(answers) === '.' ? path.resolve(answers) : path.dirname(answers));
481
- }
482
- export function getPathForFileGeneration(answers, projectRootDir) {
483
- const specAnswer = answers.specs || '';
484
- const stepDefinitionAnswer = answers.stepDefinitions || '';
485
- const pageObjectAnswer = answers.pages || '';
486
- const destSpecRootPath = generatePathfromAnswer(specAnswer, projectRootDir).replace(/\*\*$/, '');
487
- const destStepRootPath = generatePathfromAnswer(stepDefinitionAnswer, projectRootDir);
488
- const destPageObjectRootPath = answers.usePageObjects
489
- ? generatePathfromAnswer(pageObjectAnswer, projectRootDir).replace(/\*\*$/, '')
490
- : '';
491
- const destSerenityLibRootPath = usesSerenity(answers)
492
- ? path.resolve(projectRootDir, answers.serenityLibPath || 'serenity')
493
- : '';
494
- const relativePath = (answers.generateTestFiles && answers.usePageObjects)
495
- ? !(convertPackageHashToObject(answers.framework).short === 'cucumber')
496
- ? path.relative(destSpecRootPath, destPageObjectRootPath)
497
- : path.relative(destStepRootPath, destPageObjectRootPath)
498
- : '';
499
- return {
500
- destSpecRootPath: destSpecRootPath,
501
- destStepRootPath: destStepRootPath,
502
- destPageObjectRootPath: destPageObjectRootPath,
503
- destSerenityLibRootPath: destSerenityLibRootPath,
504
- relativePath: relativePath.replaceAll(path.sep, '/')
505
- };
506
- }
507
- export async function getDefaultFiles(answers, pattern) {
508
- const rootdir = await getProjectRoot(answers);
509
- const presetPackage = convertPackageHashToObject(answers.preset || '');
510
- const isJSX = TSX_BASED_FRAMEWORKS.includes(presetPackage.short || '');
511
- const val = pattern.endsWith('.feature')
512
- ? path.join(rootdir, pattern)
513
- : answers?.isUsingCompiler?.toString().includes('TypeScript')
514
- ? `${path.join(rootdir, pattern)}.ts${isJSX ? 'x' : ''}`
515
- : `${path.join(rootdir, pattern)}.js${isJSX ? 'x' : ''}`;
516
- return val;
517
- }
518
- /**
519
- * Ensure core WebdriverIO packages have the same version as cli so that if someone
520
- * installs `@wdio/cli@next` and runs the wizard, all related packages have the same version.
521
- * running `matchAll` to a version like "8.0.0-alpha.249+4bc237701", results in:
522
- * ['8.0.0-alpha.249+4bc237701', '8', '0', '0', 'alpha', '249', '4bc237701']
523
- */
524
- export function specifyVersionIfNeeded(packagesToInstall, version, npmTag) {
525
- const { value } = version.matchAll(VERSION_REGEXP).next();
526
- const [major, minor, patch, tagName, build] = (value || []).slice(1, -1); // drop commit bit
527
- return packagesToInstall.map((p) => {
528
- if ((p.startsWith('@wdio') && p !== '@wdio/visual-service') ||
529
- ['webdriver', 'webdriverio'].includes(p)) {
530
- const tag = major && npmTag === 'latest'
531
- ? `^${major}.${minor}.${patch}-${tagName}.${build}`
532
- : npmTag;
533
- return `${p}@${tag}`;
534
- }
535
- return p;
536
- });
537
- }
538
- /**
539
- * Receive project properties
540
- * @returns {@type ProjectProps} if a package.json can be found in cwd or parent directories, otherwise undefined
541
- * which means that a new project can be created
542
- */
543
- export async function getProjectProps(cwd = process.cwd()) {
544
- try {
545
- const { packageJson, path: packageJsonPath } = await readPackageUp({ cwd }) || {};
546
- if (!packageJson || !packageJsonPath) {
547
- return undefined;
548
- }
549
- return {
550
- esmSupported: (packageJson.type === 'module' ||
551
- typeof packageJson.module === 'string'),
552
- packageJson,
553
- path: path.dirname(packageJsonPath)
554
- };
555
- }
556
- catch (err) {
557
- return undefined;
558
- }
559
- }
560
- export function runProgram(command, args, options) {
561
- const child = spawn(command, args, { stdio: 'inherit', ...options });
562
- return new Promise((resolve, reject) => {
563
- let error;
564
- child.on('error', (e) => (error = e));
565
- child.on('close', code => {
566
- if (code !== 0) {
567
- return reject(new Error((error && error.message) ||
568
- `Error calling: ${command} ${args.join(' ')}`));
569
- }
570
- resolve();
571
- });
572
- });
573
- }
574
- /**
575
- * create package.json if not already existing
576
- */
577
- export async function createPackageJSON(parsedAnswers) {
578
- const packageJsonExists = await fs.access(path.resolve(process.cwd(), 'package.json')).then(() => true, () => false);
579
- // Use the exisitng package.json if it already exists.
580
- if (packageJsonExists) {
581
- return;
582
- }
583
- // If a user said no to creating a package.json, but it doesn't exist, abort.
584
- if (parsedAnswers.createPackageJSON === false) {
585
- /* istanbul ignore if */
586
- if (!packageJsonExists) {
587
- console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`);
588
- return !process.env.VITEST_WORKER_ID && process.exit(0);
589
- }
590
- return;
591
- }
592
- // Only create if the user gave explicit permission to
593
- if (parsedAnswers.createPackageJSON) {
594
- console.log(`Creating a ${chalk.bold('package.json')} for the directory...`);
595
- await fs.writeFile(path.resolve(process.cwd(), 'package.json'), JSON.stringify({
596
- name: 'webdriverio-tests',
597
- version: '0.0.0',
598
- private: true,
599
- license: 'ISC',
600
- type: 'module',
601
- dependencies: {},
602
- devDependencies: {}
603
- }, null, 2));
604
- console.log(chalk.green(chalk.bold('✔ Success!\n')));
605
- }
606
- }
607
- /**
608
- * run npm install only if required by the user
609
- */
610
- const SEP = '\n- ';
611
- export async function npmInstall(parsedAnswers, npmTag) {
612
- const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service));
613
- const presetPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.preset || '');
614
- /**
615
- * install Testing Library dependency if desired
616
- */
617
- if (parsedAnswers.installTestingLibrary && TESTING_LIBRARY_PACKAGES[presetPackage.short]) {
618
- parsedAnswers.packagesToInstall.push(TESTING_LIBRARY_PACKAGES[presetPackage.short], '@testing-library/jest-dom');
619
- }
620
- /**
621
- * add helper package for Solidjs testing
622
- */
623
- if (presetPackage.short === 'solid') {
624
- parsedAnswers.packagesToInstall.push('solid-js');
625
- }
626
- /**
627
- * add visual service if user selected support for it
628
- */
629
- if (parsedAnswers.includeVisualTesting) {
630
- parsedAnswers.packagesToInstall.push('@wdio/visual-service');
631
- }
632
- /**
633
- * add dependency for Lit testing
634
- */
635
- const preset = getPreset(parsedAnswers);
636
- if (preset === 'lit') {
637
- parsedAnswers.packagesToInstall.push('lit');
638
- }
639
- /**
640
- * add dependency for Stencil testing
641
- */
642
- if (preset === 'stencil') {
643
- parsedAnswers.packagesToInstall.push('@stencil/core');
644
- }
645
- /**
646
- * add helper for React rendering when not using Testing Library
647
- */
648
- if (presetPackage.short === 'react') {
649
- parsedAnswers.packagesToInstall.push('react');
650
- if (!parsedAnswers.installTestingLibrary) {
651
- parsedAnswers.packagesToInstall.push('react-dom');
652
- }
653
- }
654
- /**
655
- * add Jasmine types if necessary
656
- */
657
- if (parsedAnswers.framework === 'jasmine' && parsedAnswers.isUsingTypeScript) {
658
- parsedAnswers.packagesToInstall.push('@types/jasmine');
659
- }
660
- /**
661
- * add Appium mobile drivers if desired
662
- */
663
- if (parsedAnswers.purpose === 'macos') {
664
- parsedAnswers.packagesToInstall.push('appium-mac2-driver');
665
- }
666
- if (parsedAnswers.mobileEnvironment === 'android') {
667
- parsedAnswers.packagesToInstall.push('appium-uiautomator2-driver');
668
- }
669
- if (parsedAnswers.mobileEnvironment === 'ios') {
670
- parsedAnswers.packagesToInstall.push('appium-xcuitest-driver');
671
- }
672
- /**
673
- * add packages that are required by services
674
- */
675
- addServiceDeps(servicePackages, parsedAnswers.packagesToInstall);
676
- /**
677
- * update package version if CLI is a pre release
678
- */
679
- parsedAnswers.packagesToInstall = specifyVersionIfNeeded(parsedAnswers.packagesToInstall, pkg.version, npmTag);
680
- const cwd = await getProjectRoot(parsedAnswers);
681
- const pm = detectPackageManager();
682
- if (parsedAnswers.npmInstall) {
683
- console.log(`Installing packages using ${pm}:${SEP}${parsedAnswers.packagesToInstall.join(SEP)}`);
684
- const success = await installPackages(cwd, parsedAnswers.packagesToInstall, true);
685
- if (success) {
686
- console.log(chalk.green(chalk.bold('✔ Success!\n')));
687
- }
688
- }
689
- else {
690
- const installationCommand = getInstallCommand(pm, parsedAnswers.packagesToInstall, true);
691
- console.log(util.format(DEPENDENCIES_INSTALLATION_MESSAGE, installationCommand));
692
- }
693
- }
694
- /**
695
- * detect the package manager that was used
696
- */
697
- export function detectPackageManager(argv = process.argv) {
698
- return PMs.find((pm) => (
699
- // for pnpm check "~/Library/pnpm/store/v3/..."
700
- // for NPM check "~/.npm/npx/..."
701
- // for Yarn check "~/.yarn/bin/create-wdio"
702
- // for Bun check "~/.bun/bin/create-wdio"
703
- argv[1].includes(`${path.sep}${pm}${path.sep}`) ||
704
- argv[1].includes(`${path.sep}.${pm}${path.sep}`))) || 'npm';
705
- }
706
- /**
707
- * add ts-node if TypeScript is desired but not installed
708
- */
709
- export async function setupTypeScript(parsedAnswers) {
710
- /**
711
- * don't create a `tsconfig.json` if user doesn't want to use TypeScript
712
- */
713
- if (!parsedAnswers.isUsingTypeScript) {
714
- return;
715
- }
716
- /**
717
- * don't set up TypeScript if a `tsconfig.json` already exists but ensure we install `ts-node`
718
- * as it is a requirement for running TypeScript tests
719
- */
720
- if (parsedAnswers.hasRootTSConfig) {
721
- parsedAnswers.packagesToInstall.push('ts-node');
722
- return;
723
- }
724
- console.log('Setting up TypeScript...');
725
- const frameworkPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.framework);
726
- const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service));
727
- parsedAnswers.packagesToInstall.push('ts-node', 'typescript');
728
- const serenityTypes = parsedAnswers.serenityAdapter === 'jasmine' ? ['jasmine'] : [];
729
- const types = [
730
- 'node',
731
- '@wdio/globals/types',
732
- 'expect-webdriverio',
733
- ...(parsedAnswers.serenityAdapter ? serenityTypes : [frameworkPackage.package]),
734
- ...(parsedAnswers.runner === 'browser' ? ['@wdio/browser-runner'] : []),
735
- ...servicePackages
736
- .map(service => service.package)
737
- .filter(service => (
738
- /**
739
- * given that we know that all "official" services have
740
- * typescript support we only include them
741
- */
742
- service.startsWith('@wdio') ||
743
- /**
744
- * also include community maintained packages with known
745
- * support for TypeScript
746
- */
747
- COMMUNITY_PACKAGES_WITH_TS_SUPPORT.includes(service)))
748
- ];
749
- const preset = getPreset(parsedAnswers);
750
- const config = {
751
- compilerOptions: {
752
- // compiler
753
- moduleResolution: 'node',
754
- module: !parsedAnswers.esmSupport ? 'commonjs' : 'ESNext',
755
- target: 'es2022',
756
- lib: ['es2022', 'dom'],
757
- types,
758
- skipLibCheck: true,
759
- // bundler
760
- noEmit: true,
761
- allowImportingTsExtensions: true,
762
- resolveJsonModule: true,
763
- isolatedModules: true,
764
- // linting
765
- strict: true,
766
- noUnusedLocals: true,
767
- noUnusedParameters: true,
768
- noFallthroughCasesInSwitch: true,
769
- ...Object.assign(preset === 'lit'
770
- ? {
771
- experimentalDecorators: true,
772
- useDefineForClassFields: false
773
- }
774
- : {}, preset === 'react'
775
- ? {
776
- jsx: 'react-jsx'
777
- }
778
- : {}, preset === 'preact'
779
- ? {
780
- jsx: 'react-jsx',
781
- jsxImportSource: 'preact'
782
- }
783
- : {}, preset === 'solid'
784
- ? {
785
- jsx: 'preserve',
786
- jsxImportSource: 'solid-js'
787
- }
788
- : {}, preset === 'stencil'
789
- ? {
790
- experimentalDecorators: true,
791
- jsx: 'react',
792
- jsxFactory: 'h',
793
- jsxFragmentFactory: 'Fragment'
794
- }
795
- : {})
796
- },
797
- include: preset === 'svelte'
798
- ? ['src/**/*.d.ts', 'src/**/*.ts', 'src/**/*.js', 'src/**/*.svelte']
799
- : preset === 'vue'
800
- ? ['src/**/*.ts', 'src/**/*.d.ts', 'src/**/*.tsx', 'src/**/*.vue']
801
- : ['test', 'wdio.conf.ts']
802
- };
803
- await fs.mkdir(path.dirname(parsedAnswers.tsConfigFilePath), { recursive: true });
804
- await fs.writeFile(parsedAnswers.tsConfigFilePath, JSON.stringify(config, null, 4));
805
- console.log(chalk.green(chalk.bold('✔ Success!\n')));
806
- }
807
- function getPreset(parsedAnswers) {
808
- const isUsingFramework = typeof parsedAnswers.preset === 'string';
809
- return isUsingFramework ? (parsedAnswers.preset || 'lit') : '';
810
- }
811
- /**
812
- * add @babel/register package if not installed
813
- */
814
- export async function setupBabel(parsedAnswers) {
815
- if (!parsedAnswers.isUsingBabel) {
816
- return;
817
- }
818
- if (!await hasPackage('@babel/register')) {
819
- parsedAnswers.packagesToInstall.push('@babel/register');
820
- }
821
- /**
822
- * setup Babel if no config file exists
823
- */
824
- const hasBabelConfig = await Promise.all([
825
- fs.access(path.join(parsedAnswers.projectRootDir, 'babel.js')),
826
- fs.access(path.join(parsedAnswers.projectRootDir, 'babel.cjs')),
827
- fs.access(path.join(parsedAnswers.projectRootDir, 'babel.mjs')),
828
- fs.access(path.join(parsedAnswers.projectRootDir, '.babelrc'))
829
- ]).then((results) => results.filter(Boolean).length > 1, () => false);
830
- if (!hasBabelConfig) {
831
- console.log('Setting up Babel project...');
832
- if (!await hasPackage('@babel/core')) {
833
- parsedAnswers.packagesToInstall.push('@babel/core');
834
- }
835
- if (!await hasPackage('@babel/preset-env')) {
836
- parsedAnswers.packagesToInstall.push('@babel/preset-env');
837
- }
838
- await fs.writeFile(path.join(process.cwd(), 'babel.config.js'), `module.exports = ${JSON.stringify({
839
- presets: [
840
- ['@babel/preset-env', {
841
- targets: {
842
- node: 18
843
- }
844
- }]
845
- ]
846
- }, null, 4)}`);
847
- console.log(chalk.green(chalk.bold('✔ Success!\n')));
848
- }
849
- }
850
- export async function createWDIOConfig(parsedAnswers) {
851
- try {
852
- console.log('Creating a WebdriverIO config file...');
853
- const tplPath = path.resolve(__dirname, 'templates', 'wdio.conf.tpl.ejs');
854
- const renderedTpl = await renderFile(tplPath, {
855
- answers: parsedAnswers,
856
- _: new EjsHelpers({ useEsm: parsedAnswers.esmSupport, useTypeScript: parsedAnswers.isUsingTypeScript })
857
- });
858
- await fs.writeFile(parsedAnswers.wdioConfigPath, renderedTpl);
859
- console.log(chalk.green(chalk.bold('✔ Success!\n')));
860
- if (parsedAnswers.generateTestFiles) {
861
- console.log('Autogenerating test files...');
862
- await generateTestFiles(parsedAnswers);
863
- console.log(chalk.green(chalk.bold('✔ Success!\n')));
864
- }
865
- }
866
- catch (err) {
867
- throw new Error(`⚠️ Couldn't write config file: ${err.stack}`);
868
- }
869
- }
870
- /**
871
- * Get project root directory based on questionair answers
872
- * @param answers questionair answers
873
- * @param projectProps project properties received via `getProjectProps`
874
- * @returns project root path
875
- */
876
- export async function getProjectRoot(parsedAnswers) {
877
- const root = (await getProjectProps())?.path;
878
- if (!root) {
879
- throw new Error('Could not find project root directory with a package.json');
880
- }
881
- return !parsedAnswers || parsedAnswers.projectRootCorrect
882
- ? root
883
- : parsedAnswers.projectRoot || process.cwd();
884
- }
885
- export async function createWDIOScript(parsedAnswers) {
886
- const rootDir = await getProjectRoot(parsedAnswers);
887
- const pathToWdioConfig = `./${path.join('.', parsedAnswers.wdioConfigPath.replace(rootDir, ''))}`;
888
- const wdioScripts = {
889
- 'wdio': `wdio run ${pathToWdioConfig}`,
890
- };
891
- const serenityScripts = {
892
- 'serenity': 'failsafe serenity:update serenity:clean wdio serenity:report',
893
- 'serenity:update': 'serenity-bdd update',
894
- 'serenity:clean': 'rimraf target',
895
- 'wdio': `wdio run ${pathToWdioConfig}`,
896
- 'serenity:report': 'serenity-bdd run',
897
- };
898
- const scripts = parsedAnswers.serenityAdapter ? serenityScripts : wdioScripts;
899
- for (const [script, command] of Object.entries(scripts)) {
900
- const args = ['pkg', 'set', `scripts.${script}=${command}`];
901
- try {
902
- console.log(`Adding ${chalk.bold(`"${script}"`)} script to package.json`);
903
- await runProgram(NPM_COMMAND, args, { cwd: parsedAnswers.projectRootDir });
904
- }
905
- catch (err) {
906
- const [preArgs, scriptPath] = args.join(' ').split('=');
907
- console.error(`⚠️ Couldn't add script to package.json: "${err.message}", you can add it manually ` +
908
- `by running:\n\n\t${NPM_COMMAND} ${preArgs}="${scriptPath}"`);
909
- return false;
910
- }
911
- }
912
- console.log(chalk.green(chalk.bold('✔ Success!')));
913
- return true;
914
- }
915
- export async function runAppiumInstaller(parsedAnswers) {
916
- if (parsedAnswers.e2eEnvironment !== 'mobile') {
917
- return;
918
- }
919
- const answer = await inquirer.prompt({
920
- name: 'continueWithAppiumSetup',
921
- message: 'Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?',
922
- type: 'confirm',
923
- default: true
924
- });
925
- if (!answer.continueWithAppiumSetup) {
926
- return console.log('Ok! You can learn more about setting up mobile environments in the ' +
927
- 'Appium docs at https://appium.io/docs/en/2.0/quickstart/');
928
- }
929
- return $({ stdio: 'inherit' }) `npx appium-installer`;
930
- }