UNPKG

39.7 kBJavaScriptView Raw
1import fs from 'node:fs/promises';
2import path from 'node:path';
3import { createRequire } from 'node:module';
4import { HOOK_DEFINITION } from '@wdio/utils';
5import { detectCompiler, getDefaultFiles, convertPackageHashToObject, getProjectRoot, detectPackageManager, } from './utils.js';
6const require = createRequire(import.meta.url);
7export const pkg = require('../package.json');
8export const CLI_EPILOGUE = `Documentation: https://webdriver.io\n@wdio/cli (v${pkg.version})`;
9export const CONFIG_HELPER_INTRO = `
10===============================
11🤖 WDIO Configuration Wizard 🧙
12===============================
13`;
14export const PMs = ['npm', 'yarn', 'pnpm', 'bun'];
15export const SUPPORTED_CONFIG_FILE_EXTENSION = ['js', 'ts', 'mjs', 'mts', 'cjs', 'cts'];
16export const configHelperSuccessMessage = ({ projectRootDir, runScript, extraInfo = '' }) => `
17🤖 Successfully setup project at ${projectRootDir} 🎉
18
19Join our Discord Community Server and instantly find answers to your issues or queries. Or just join and say hi 👋!
20 🔗 https://discord.webdriver.io
21
22Visit the project on GitHub to report bugs 🐛 or raise feature requests 💡:
23 🔗 https://github.com/webdriverio/webdriverio
24${extraInfo}
25To run your tests, execute:
26$ cd ${projectRootDir}
27$ npm run ${runScript}
28`;
29export const CONFIG_HELPER_SERENITY_BANNER = `
30Learn more about Serenity/JS:
31 🔗 https://serenity-js.org
32`;
33export const DEPENDENCIES_INSTALLATION_MESSAGE = `
34To install dependencies, execute:
35%s
36`;
37export const NPM_INSTALL = '';
38export const ANDROID_CONFIG = {
39 platformName: 'Android',
40 automationName: 'UiAutomator2',
41 deviceName: 'Test'
42};
43export const IOS_CONFIG = {
44 platformName: 'iOS',
45 automationName: 'XCUITest',
46 deviceName: 'iPhone Simulator'
47};
48export var CompilerOptions;
49(function (CompilerOptions) {
50 CompilerOptions["Babel"] = "Babel (https://babeljs.io/)";
51 CompilerOptions["TS"] = "TypeScript (https://www.typescriptlang.org/)";
52 CompilerOptions["Nil"] = "No!";
53})(CompilerOptions || (CompilerOptions = {}));
54/**
55 * We have to use a string hash for value because InquirerJS default values do not work if we have
56 * objects as a `value` to be stored from the user's answers.
57 */
58export const SUPPORTED_PACKAGES = {
59 runner: [
60 { name: 'E2E Testing - of Web or Mobile Applications', value: '@wdio/local-runner$--$local$--$e2e' },
61 { name: 'Component or Unit Testing - in the browser\n > https://webdriver.io/docs/component-testing', value: '@wdio/browser-runner$--$browser$--$component' },
62 { name: 'Desktop Testing - of Electron Applications\n > https://webdriver.io/docs/desktop-testing/electron', value: '@wdio/local-runner$--$local$--$electron' },
63 { name: 'Desktop Testing - of MacOS Applications\n > https://webdriver.io/docs/desktop-testing/macos', value: '@wdio/local-runner$--$local$--$macos' },
64 { name: 'VS Code Extension Testing\n > https://webdriver.io/docs/vscode-extension-testing', value: '@wdio/local-runner$--$local$--$vscode' }
65 ],
66 framework: [
67 { name: 'Mocha (https://mochajs.org/)', value: '@wdio/mocha-framework$--$mocha' },
68 { name: 'Mocha with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha' },
69 { name: 'Jasmine (https://jasmine.github.io/)', value: '@wdio/jasmine-framework$--$jasmine' },
70 { name: 'Jasmine with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine' },
71 { name: 'Cucumber (https://cucumber.io/)', value: '@wdio/cucumber-framework$--$cucumber' },
72 { name: 'Cucumber with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber' },
73 ],
74 reporter: [
75 { name: 'spec', value: '@wdio/spec-reporter$--$spec' },
76 { name: 'dot', value: '@wdio/dot-reporter$--$dot' },
77 { name: 'junit', value: '@wdio/junit-reporter$--$junit' },
78 { name: 'allure', value: '@wdio/allure-reporter$--$allure' },
79 { name: 'sumologic', value: '@wdio/sumologic-reporter$--$sumologic' },
80 { name: 'concise', value: '@wdio/concise-reporter$--$concise' },
81 { name: 'json', value: '@wdio/json-reporter$--$json' },
82 // external
83 { name: 'reportportal', value: 'wdio-reportportal-reporter$--$reportportal' },
84 { name: 'video', value: 'wdio-video-reporter$--$video' },
85 { name: 'cucumber-json', value: 'wdio-cucumberjs-json-reporter$--$cucumberjs-json' },
86 { name: 'mochawesome', value: 'wdio-mochawesome-reporter$--$mochawesome' },
87 { name: 'timeline', value: 'wdio-timeline-reporter$--$timeline' },
88 { name: 'html-nice', value: 'wdio-html-nice-reporter$--$html-nice' },
89 { name: 'slack', value: '@moroo/wdio-slack-reporter$--$slack' },
90 { name: 'teamcity', value: 'wdio-teamcity-reporter$--$teamcity' },
91 { name: 'delta', value: '@delta-reporter/wdio-delta-reporter-service$--$delta' },
92 { name: 'testrail', value: '@wdio/testrail-reporter$--$testrail' },
93 { name: 'light', value: 'wdio-light-reporter$--$light' }
94 ],
95 plugin: [
96 { name: 'wait-for: utilities that provide functionalities to wait for certain conditions till a defined task is complete.\n > https://www.npmjs.com/package/wdio-wait-for', value: 'wdio-wait-for$--$wait-for' },
97 { name: 'angular-component-harnesses: support for Angular component test harnesses\n > https://www.npmjs.com/package/@badisi/wdio-harness', value: '@badisi/wdio-harness$--$harness' },
98 { name: 'Testing Library: utilities that encourage good testing practices laid down by dom-testing-library.\n > https://testing-library.com/docs/webdriverio-testing-library/intro', value: '@testing-library/webdriverio$--$testing-library' }
99 ],
100 service: [
101 // internal or community driver services
102 { name: 'visual', value: '@wdio/visual-service$--$visual' },
103 { name: 'vite', value: 'wdio-vite-service$--$vite' },
104 { name: 'nuxt', value: 'wdio-nuxt-service$--$nuxt' },
105 { name: 'firefox-profile', value: '@wdio/firefox-profile-service$--$firefox-profile' },
106 { name: 'gmail', value: 'wdio-gmail-service$--$gmail' },
107 { name: 'sauce', value: '@wdio/sauce-service$--$sauce' },
108 { name: 'testingbot', value: '@wdio/testingbot-service$--$testingbot' },
109 { name: 'crossbrowsertesting', value: '@wdio/crossbrowsertesting-service$--$crossbrowsertesting' },
110 { name: 'browserstack', value: '@wdio/browserstack-service$--$browserstack' },
111 { name: 'devtools', value: '@wdio/devtools-service$--$devtools' },
112 { name: 'vscode', value: 'wdio-vscode-service$--$vscode' },
113 { name: 'electron', value: 'wdio-electron-service$--$electron' },
114 { name: 'appium', value: '@wdio/appium-service$--$appium' },
115 // external
116 { name: 'eslinter-service', value: 'wdio-eslinter-service$--$eslinter' },
117 { name: 'lambdatest', value: 'wdio-lambdatest-service$--$lambdatest' },
118 { name: 'zafira-listener', value: 'wdio-zafira-listener-service$--$zafira-listener' },
119 { name: 'reportportal', value: 'wdio-reportportal-service$--$reportportal' },
120 { name: 'docker', value: 'wdio-docker-service$--$docker' },
121 { name: 'ui5', value: 'wdio-ui5-service$--$ui5' },
122 { name: 'wiremock', value: 'wdio-wiremock-service$--$wiremock' },
123 { name: 'ng-apimock', value: 'wdio-ng-apimock-service$--$ng-apimock' },
124 { name: 'slack', value: 'wdio-slack-service$--$slack' },
125 { name: 'cucumber-viewport-logger', value: 'wdio-cucumber-viewport-logger-service$--$cucumber-viewport-logger' },
126 { name: 'intercept', value: 'wdio-intercept-service$--$intercept' },
127 { name: 'docker', value: 'wdio-docker-service$--$docker' },
128 { name: 'novus-visual-regression', value: 'wdio-novus-visual-regression-service$--$novus-visual-regression' },
129 { name: 'rerun', value: 'wdio-rerun-service$--$rerun' },
130 { name: 'winappdriver', value: 'wdio-winappdriver-service$--$winappdriver' },
131 { name: 'ywinappdriver', value: 'wdio-ywinappdriver-service$--$ywinappdriver' },
132 { name: 'performancetotal', value: 'wdio-performancetotal-service$--$performancetotal' },
133 { name: 'cleanuptotal', value: 'wdio-cleanuptotal-service$--$cleanuptotal' },
134 { name: 'aws-device-farm', value: 'wdio-aws-device-farm-service$--$aws-device-farm' },
135 { name: 'ocr-native-apps', value: 'wdio-ocr-service$--$ocr-native-apps' },
136 { name: 'ms-teams', value: 'wdio-ms-teams-service$--$ms-teams' },
137 { name: 'tesults', value: 'wdio-tesults-service$--$tesults' },
138 { name: 'azure-devops', value: '@gmangiapelo/wdio-azure-devops-service$--$azure-devops' },
139 { name: 'google-Chat', value: 'wdio-google-chat-service$--$google-chat' },
140 { name: 'qmate-service', value: '@sap_oss/wdio-qmate-service$--$qmate-service' },
141 { name: 'vitaqai', value: 'wdio-vitaqai-service$--$vitaqai' },
142 { name: 'robonut', value: 'wdio-robonut-service$--$robonut' },
143 { name: 'qunit', value: 'wdio-qunit-service$--$qunit' }
144 ]
145};
146export const SUPPORTED_BROWSER_RUNNER_PRESETS = [
147 { name: 'Lit (https://lit.dev/)', value: '$--$' },
148 { name: 'Vue.js (https://vuejs.org/)', value: '@vitejs/plugin-vue$--$vue' },
149 { name: 'Svelte (https://svelte.dev/)', value: '@sveltejs/vite-plugin-svelte$--$svelte' },
150 { name: 'SolidJS (https://www.solidjs.com/)', value: 'vite-plugin-solid$--$solid' },
151 { name: 'StencilJS (https://stenciljs.com/)', value: '$--$stencil' },
152 { name: 'React (https://reactjs.org/)', value: '@vitejs/plugin-react$--$react' },
153 { name: 'Preact (https://preactjs.com/)', value: '@preact/preset-vite$--$preact' },
154 { name: 'Other', value: false }
155];
156export const TESTING_LIBRARY_PACKAGES = {
157 react: '@testing-library/react',
158 preact: '@testing-library/preact',
159 vue: '@testing-library/vue',
160 svelte: '@testing-library/svelte',
161 solid: 'solid-testing-library'
162};
163export var BackendChoice;
164(function (BackendChoice) {
165 BackendChoice["Local"] = "On my local machine";
166 BackendChoice["Experitest"] = "In the cloud using Experitest";
167 BackendChoice["Saucelabs"] = "In the cloud using Sauce Labs";
168 BackendChoice["Browserstack"] = "In the cloud using BrowserStack";
169 BackendChoice["OtherVendors"] = "In the cloud using Testingbot or LambdaTest or a different service";
170 BackendChoice["Grid"] = "I have my own Selenium cloud";
171})(BackendChoice || (BackendChoice = {}));
172export var ElectronBuildToolChoice;
173(function (ElectronBuildToolChoice) {
174 ElectronBuildToolChoice["ElectronForge"] = "Electron Forge (https://www.electronforge.io/)";
175 ElectronBuildToolChoice["ElectronBuilder"] = "electron-builder (https://www.electron.build/)";
176 ElectronBuildToolChoice["SomethingElse"] = "Something else";
177})(ElectronBuildToolChoice || (ElectronBuildToolChoice = {}));
178var ProtocolOptions;
179(function (ProtocolOptions) {
180 ProtocolOptions["HTTPS"] = "https";
181 ProtocolOptions["HTTP"] = "http";
182})(ProtocolOptions || (ProtocolOptions = {}));
183export var RegionOptions;
184(function (RegionOptions) {
185 RegionOptions["US"] = "us";
186 RegionOptions["EU"] = "eu";
187 RegionOptions["APAC"] = "apac";
188})(RegionOptions || (RegionOptions = {}));
189export const E2E_ENVIRONMENTS = [
190 { name: 'Web - web applications in the browser', value: 'web' },
191 { name: 'Mobile - native, hybrid and mobile web apps, on Android or iOS', value: 'mobile' }
192];
193export const MOBILE_ENVIRONMENTS = [
194 { name: 'Android - native, hybrid and mobile web apps, tested on emulators and real devices\n > using UiAutomator2 (https://www.npmjs.com/package/appium-uiautomator2-driver)', value: 'android' },
195 { name: 'iOS - applications on iOS, iPadOS, and tvOS\n > using XCTest (https://appium.github.io/appium-xcuitest-driver)', value: 'ios' }
196];
197export const BROWSER_ENVIRONMENTS = [
198 { name: 'Chrome', value: 'chrome' },
199 { name: 'Firefox', value: 'firefox' },
200 { name: 'Safari', value: 'safari' },
201 { name: 'Microsoft Edge', value: 'MicrosoftEdge' }
202];
203function isBrowserRunner(answers) {
204 return answers.runner === SUPPORTED_PACKAGES.runner[1].value;
205}
206export function usesSerenity(answers) {
207 return answers.framework.includes('serenity-js');
208}
209function getTestingPurpose(answers) {
210 return convertPackageHashToObject(answers.runner).purpose;
211}
212export const isNuxtProject = await Promise.all([
213 path.join(process.cwd(), 'nuxt.config.js'),
214 path.join(process.cwd(), 'nuxt.config.ts'),
215 path.join(process.cwd(), 'nuxt.config.mjs'),
216 path.join(process.cwd(), 'nuxt.config.mts')
217].map((p) => fs.access(p).then(() => true, () => false))).then((res) => res.some(Boolean), () => false);
218function selectDefaultService(serviceNames) {
219 serviceNames = Array.isArray(serviceNames) ? serviceNames : [serviceNames];
220 return SUPPORTED_PACKAGES.service
221 /* istanbul ignore next */
222 .filter(({ name }) => serviceNames.includes(name))
223 .map(({ value }) => value);
224}
225function prioServiceOrderFor(serviceNamesParam) {
226 const serviceNames = Array.isArray(serviceNamesParam) ? serviceNamesParam : [serviceNamesParam];
227 let services = SUPPORTED_PACKAGES.service;
228 for (const serviceName of serviceNames) {
229 const index = services.findIndex(({ name }) => name === serviceName);
230 services = [services[index], ...services.slice(0, index), ...services.slice(index + 1)];
231 }
232 return services;
233}
234export const QUESTIONNAIRE = [{
235 type: 'list',
236 name: 'runner',
237 message: 'What type of testing would you like to do?',
238 choices: SUPPORTED_PACKAGES.runner
239 }, {
240 type: 'list',
241 name: 'preset',
242 message: 'Which framework do you use for building components?',
243 choices: SUPPORTED_BROWSER_RUNNER_PRESETS,
244 // only ask if there are more than 1 runner to pick from
245 when: /* istanbul ignore next */ isBrowserRunner
246 }, {
247 type: 'confirm',
248 name: 'installTestingLibrary',
249 message: 'Do you like to use Testing Library (https://testing-library.com/) as test utility?',
250 default: true,
251 // only ask if there are more than 1 runner to pick from
252 when: /* istanbul ignore next */ (answers) => (isBrowserRunner(answers) &&
253 /**
254 * Only show if Testing Library has an add-on for framework
255 */
256 answers.preset && TESTING_LIBRARY_PACKAGES[convertPackageHashToObject(answers.preset).short])
257 }, {
258 type: 'list',
259 name: 'electronBuildTool',
260 message: 'Which tool are you using to build your Electron app?',
261 choices: Object.values(ElectronBuildToolChoice),
262 when: /* instanbul ignore next */ (answers) => getTestingPurpose(answers) === 'electron'
263 }, {
264 type: 'input',
265 name: 'electronAppBinaryPath',
266 message: 'What is the path to the binary of your built Electron app?',
267 when: /* istanbul ignore next */ (answers) => getTestingPurpose(answers) === 'electron' && (answers.electronBuildTool === ElectronBuildToolChoice.SomethingElse)
268 }, {
269 type: 'list',
270 name: 'backend',
271 message: 'Where is your automation backend located?',
272 choices: Object.values(BackendChoice),
273 when: /* instanbul ignore next */ (answers) => getTestingPurpose(answers) === 'e2e'
274 }, {
275 type: 'list',
276 name: 'e2eEnvironment',
277 message: 'Which environment you would like to automate?',
278 choices: E2E_ENVIRONMENTS,
279 default: 'web',
280 when: /* istanbul ignore next */ (answers) => getTestingPurpose(answers) === 'e2e'
281 }, {
282 type: 'list',
283 name: 'mobileEnvironment',
284 message: 'Which mobile environment you\'ld like to automate?',
285 choices: MOBILE_ENVIRONMENTS,
286 when: /* instanbul ignore next */ (answers) => (getTestingPurpose(answers) === 'e2e' &&
287 answers.e2eEnvironment === 'mobile')
288 }, {
289 type: 'checkbox',
290 name: 'browserEnvironment',
291 message: 'With which browser should we start?',
292 choices: BROWSER_ENVIRONMENTS,
293 default: ['chrome'],
294 when: /* instanbul ignore next */ (answers) => (getTestingPurpose(answers) === 'e2e' &&
295 answers.e2eEnvironment === 'web')
296 }, {
297 type: 'input',
298 name: 'hostname',
299 message: 'What is the host address of that cloud service?',
300 when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.indexOf('different service') > -1
301 }, {
302 type: 'input',
303 name: 'port',
304 message: 'What is the port on which that service is running?',
305 default: '80',
306 when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.indexOf('different service') > -1
307 }, {
308 type: 'input',
309 name: 'expEnvAccessKey',
310 message: 'Access key from Experitest Cloud',
311 default: 'EXPERITEST_ACCESS_KEY',
312 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Experitest
313 }, {
314 type: 'input',
315 name: 'expEnvHostname',
316 message: 'Environment variable for cloud url',
317 default: 'example.experitest.com',
318 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Experitest
319 }, {
320 type: 'input',
321 name: 'expEnvPort',
322 message: 'Environment variable for port',
323 default: '443',
324 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Experitest
325 }, {
326 type: 'list',
327 name: 'expEnvProtocol',
328 message: 'Choose a protocol for environment variable',
329 default: ProtocolOptions.HTTPS,
330 choices: Object.values(ProtocolOptions),
331 when: /* istanbul ignore next */ (answers) => (answers.backend === BackendChoice.Experitest &&
332 answers.expEnvPort !== '80' &&
333 answers.expEnvPort !== '443')
334 }, {
335 type: 'input',
336 name: 'env_user',
337 message: 'Environment variable for username',
338 default: 'LT_USERNAME',
339 when: /* istanbul ignore next */ (answers) => (answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
340 answers.hostname.indexOf('lambdatest.com') > -1)
341 }, {
342 type: 'input',
343 name: 'env_key',
344 message: 'Environment variable for access key',
345 default: 'LT_ACCESS_KEY',
346 when: /* istanbul ignore next */ (answers) => (answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
347 answers.hostname.indexOf('lambdatest.com') > -1)
348 }, {
349 type: 'input',
350 name: 'env_user',
351 message: 'Environment variable for username',
352 default: 'BROWSERSTACK_USERNAME',
353 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Browserstack
354 }, {
355 type: 'input',
356 name: 'env_key',
357 message: 'Environment variable for access key',
358 default: 'BROWSERSTACK_ACCESS_KEY',
359 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Browserstack
360 }, {
361 type: 'input',
362 name: 'env_user',
363 message: 'Environment variable for username',
364 default: 'SAUCE_USERNAME',
365 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Saucelabs
366 }, {
367 type: 'input',
368 name: 'env_key',
369 message: 'Environment variable for access key',
370 default: 'SAUCE_ACCESS_KEY',
371 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Saucelabs
372 }, {
373 type: 'list',
374 name: 'region',
375 message: 'In which region do you want to run your Sauce Labs tests in?',
376 choices: Object.values(RegionOptions),
377 when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Saucelabs
378 }, {
379 type: 'confirm',
380 name: 'useSauceConnect',
381 message: ('Are you testing a local application and need Sauce Connect to be set-up?\n' +
382 'Read more on Sauce Connect at: https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy'),
383 default: isNuxtProject,
384 when: /* istanbul ignore next */ (answers) => (answers.backend === BackendChoice.Saucelabs &&
385 !isNuxtProject)
386 }, {
387 type: 'input',
388 name: 'hostname',
389 message: 'What is the IP or URI to your Selenium standalone or grid server?',
390 default: 'localhost',
391 when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
392 }, {
393 type: 'input',
394 name: 'port',
395 message: 'What is the port which your Selenium standalone or grid server is running on?',
396 default: '4444',
397 when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
398 }, {
399 type: 'input',
400 name: 'path',
401 message: 'What is the path to your browser driver or grid server?',
402 default: '/',
403 when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1
404 }, {
405 type: 'list',
406 name: 'framework',
407 message: 'Which framework do you want to use?',
408 choices: /* instanbul ignore next */ (answers) => {
409 /**
410 * browser runner currently supports only Mocha framework
411 */
412 if (isBrowserRunner(answers)) {
413 return SUPPORTED_PACKAGES.framework.slice(0, 1);
414 }
415 /**
416 * Serenity tests don't come with proper ElectronJS example files
417 */
418 if (getTestingPurpose(answers) === 'electron') {
419 return SUPPORTED_PACKAGES.framework.filter(({ value }) => !value.startsWith('@serenity-js'));
420 }
421 return SUPPORTED_PACKAGES.framework;
422 }
423 }, {
424 type: 'list',
425 name: 'isUsingCompiler',
426 message: 'Do you want to use a compiler?',
427 choices: (answers) => {
428 /**
429 * StencilJS only supports TypeScript
430 */
431 if (answers.preset && answers.preset.includes('stencil')) {
432 return [CompilerOptions.TS];
433 }
434 return Object.values(CompilerOptions);
435 },
436 default: /* istanbul ignore next */ (answers) => detectCompiler(answers)
437 }, {
438 type: 'confirm',
439 name: 'generateTestFiles',
440 message: 'Do you want WebdriverIO to autogenerate some test files?',
441 default: true,
442 when: /* istanbul ignore next */ (answers) => {
443 /**
444 * we only have examples for Mocha and Jasmine
445 */
446 if (['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) && answers.framework.includes('cucumber')) {
447 return false;
448 }
449 return true;
450 }
451 }, {
452 type: 'input',
453 name: 'specs',
454 message: 'What should be the location of your spec files?',
455 default: /* istanbul ignore next */ (answers) => {
456 const pattern = isBrowserRunner(answers) ? 'src/**/*.test' : 'test/specs/**/*';
457 return getDefaultFiles(answers, pattern);
458 },
459 when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.framework.match(/(mocha|jasmine)/)
460 }, {
461 type: 'input',
462 name: 'specs',
463 message: 'What should be the location of your feature files?',
464 default: (answers) => getDefaultFiles(answers, 'features/**/*.feature'),
465 when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.framework.includes('cucumber')
466 }, {
467 type: 'input',
468 name: 'stepDefinitions',
469 message: 'What should be the location of your step definitions?',
470 default: (answers) => getDefaultFiles(answers, 'features/step-definitions/steps'),
471 when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.framework.includes('cucumber')
472 }, {
473 type: 'confirm',
474 name: 'usePageObjects',
475 message: 'Do you want to use page objects (https://martinfowler.com/bliki/PageObject.html)?',
476 default: true,
477 when: /* istanbul ignore next */ (answers) => (answers.generateTestFiles &&
478 /**
479 * page objects aren't common for component testing
480 */
481 !isBrowserRunner(answers) &&
482 /**
483 * and also not needed when running VS Code tests since the service comes with
484 * its own page object implementation, nor when running Electron or MacOS tests
485 */
486 !['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) &&
487 /**
488 * Serenity/JS generates Lean Page Objects by default, so there's no need to ask about it
489 * See https://serenity-js.org/handbook/web-testing/page-objects-pattern/
490 */
491 !usesSerenity(answers))
492 }, {
493 type: 'input',
494 name: 'pages',
495 message: 'Where are your page objects located?',
496 default: /* istanbul ignore next */ (answers) => (answers.framework.match(/(mocha|jasmine)/)
497 ? getDefaultFiles(answers, 'test/pageobjects/**/*')
498 : getDefaultFiles(answers, 'features/pageobjects/**/*')),
499 when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.usePageObjects
500 }, {
501 type: 'input',
502 name: 'serenityLibPath',
503 message: 'What should be the location of your Serenity/JS Screenplay Pattern library?',
504 default: /* istanbul ignore next */ async (answers) => {
505 const projectRootDir = await getProjectRoot(answers);
506 const specsDir = path.resolve(projectRootDir, path.dirname(answers.specs || '').replace(/\*\*$/, ''));
507 return path.resolve(specsDir, '..', 'serenity');
508 },
509 when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && usesSerenity(answers)
510 }, {
511 type: 'checkbox',
512 name: 'reporters',
513 message: 'Which reporter do you want to use?',
514 choices: SUPPORTED_PACKAGES.reporter,
515 // @ts-ignore
516 default: [SUPPORTED_PACKAGES.reporter.find(
517 /* istanbul ignore next */
518 ({ name }) => name === 'spec').value
519 ]
520 }, {
521 type: 'checkbox',
522 name: 'plugins',
523 message: 'Do you want to add a plugin to your test setup?',
524 choices: SUPPORTED_PACKAGES.plugin,
525 default: []
526 }, {
527 type: 'confirm',
528 name: 'includeVisualTesting',
529 message: 'Would you like to include Visual Testing to your setup? For more information see https://webdriver.io/docs/visual-testing!',
530 default: false,
531 when: /* istanbul ignore next */ (answers) => {
532 /**
533 * visual testing mostly makes sense for e2e and component tests
534 */
535 return ['e2e', 'component'].includes(getTestingPurpose(answers));
536 }
537 }, {
538 type: 'checkbox',
539 name: 'services',
540 message: 'Do you want to add a service to your test setup?',
541 choices: (answers) => {
542 const services = [];
543 if (answers.backend === BackendChoice.Browserstack) {
544 services.push('browserstack');
545 }
546 else if (answers.backend === BackendChoice.Saucelabs) {
547 services.push('sauce');
548 }
549 if (answers.e2eEnvironment === 'mobile') {
550 services.push('appium');
551 }
552 if (getTestingPurpose(answers) === 'e2e' && isNuxtProject) {
553 services.push('nuxt');
554 }
555 if (getTestingPurpose(answers) === 'vscode') {
556 return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'vscode')];
557 }
558 else if (getTestingPurpose(answers) === 'electron') {
559 return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'electron')];
560 }
561 else if (getTestingPurpose(answers) === 'macos') {
562 return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'appium')];
563 }
564 return prioServiceOrderFor(services);
565 },
566 default: (answers) => {
567 const defaultServices = [];
568 if (answers.backend === BackendChoice.Browserstack) {
569 defaultServices.push('browserstack');
570 }
571 else if (answers.backend === BackendChoice.Saucelabs) {
572 defaultServices.push('sauce');
573 }
574 if (answers.e2eEnvironment === 'mobile' || getTestingPurpose(answers) === 'macos') {
575 defaultServices.push('appium');
576 }
577 if (getTestingPurpose(answers) === 'vscode') {
578 defaultServices.push('vscode');
579 }
580 else if (getTestingPurpose(answers) === 'electron') {
581 defaultServices.push('electron');
582 }
583 if (isNuxtProject) {
584 defaultServices.push('nuxt');
585 }
586 if (answers.includeVisualTesting) {
587 defaultServices.push('visual');
588 }
589 return selectDefaultService(defaultServices);
590 }
591 }, {
592 type: 'input',
593 name: 'outputDir',
594 message: 'In which directory should the xunit reports get stored?',
595 default: './',
596 when: /* istanbul ignore next */ (answers) => answers.reporters.includes('junit')
597 }, {
598 type: 'input',
599 name: 'outputDir',
600 message: 'In which directory should the json reports get stored?',
601 default: './',
602 when: /* istanbul ignore next */ (answers) => answers.reporters.includes('json')
603 }, {
604 type: 'input',
605 name: 'outputDir',
606 message: 'In which directory should the mochawesome json reports get stored?',
607 default: './',
608 when: /* istanbul ignore next */ (answers) => answers.reporters.includes('mochawesome')
609 }, {
610 type: 'confirm',
611 name: 'npmInstall',
612 message: () => `Do you want me to run \`${detectPackageManager()} install\``,
613 default: true
614 }];
615const SUPPORTED_SNAPSHOTSTATE_OPTIONS = ['all', 'new', 'none'];
616export const COMMUNITY_PACKAGES_WITH_TS_SUPPORT = [
617 'wdio-electron-service',
618 'wdio-vscode-service',
619 'wdio-nuxt-service',
620 'wdio-vite-service',
621 'wdio-gmail-service'
622];
623export const TESTRUNNER_DEFAULTS = {
624 /**
625 * Define specs for test execution. You can either specify a glob
626 * pattern to match multiple files at once or wrap a glob or set of
627 * paths into an array to run them within a single worker process.
628 */
629 specs: {
630 type: 'object',
631 validate: (param) => {
632 if (!Array.isArray(param)) {
633 throw new Error('the "specs" option needs to be a list of strings');
634 }
635 }
636 },
637 /**
638 * exclude specs from test execution
639 */
640 exclude: {
641 type: 'object',
642 validate: (param) => {
643 if (!Array.isArray(param)) {
644 throw new Error('the "exclude" option needs to be a list of strings');
645 }
646 }
647 },
648 /**
649 * key/value definition of suites (named by key) and a list of specs as value
650 * to specify a specific set of tests to execute
651 */
652 suites: {
653 type: 'object'
654 },
655 /**
656 * Project root directory path.
657 */
658 rootDir: {
659 type: 'string'
660 },
661 /**
662 * If you only want to run your tests until a specific amount of tests have failed use
663 * bail (default is 0 - don't bail, run all tests).
664 */
665 bail: {
666 type: 'number',
667 default: 0
668 },
669 /**
670 * supported test framework by wdio testrunner
671 */
672 framework: {
673 type: 'string'
674 },
675 /**
676 * capabilities of WebDriver sessions
677 */
678 capabilities: {
679 type: 'object',
680 validate: (param) => {
681 /**
682 * should be an object
683 */
684 if (!Array.isArray(param)) {
685 if (typeof param === 'object') {
686 return true;
687 }
688 throw new Error('the "capabilities" options needs to be an object or a list of objects');
689 }
690 /**
691 * or an array of objects
692 */
693 for (const option of param) {
694 if (typeof option === 'object') { // Check does not work recursively
695 continue;
696 }
697 throw new Error('expected every item of a list of capabilities to be of type object');
698 }
699 return true;
700 },
701 required: true
702 },
703 /**
704 * list of reporters to use, a reporter can be either a string or an object with
705 * reporter options, e.g.:
706 * [
707 * 'dot',
708 * {
709 * name: 'spec',
710 * outputDir: __dirname + '/reports'
711 * }
712 * ]
713 */
714 reporters: {
715 type: 'object',
716 validate: (param) => {
717 /**
718 * option must be an array
719 */
720 if (!Array.isArray(param)) {
721 throw new Error('the "reporters" options needs to be a list of strings');
722 }
723 const isValidReporter = (option) => ((typeof option === 'string') ||
724 (typeof option === 'function'));
725 /**
726 * array elements must be:
727 */
728 for (const option of param) {
729 /**
730 * either a string or a function (custom reporter)
731 */
732 if (isValidReporter(option)) {
733 continue;
734 }
735 /**
736 * or an array with the name of the reporter as first element and the options
737 * as second element
738 */
739 if (Array.isArray(option) &&
740 typeof option[1] === 'object' &&
741 isValidReporter(option[0])) {
742 continue;
743 }
744 throw new Error('a reporter should be either a string in the format "wdio-<reportername>-reporter" ' +
745 'or a function/class. Please see the docs for more information on custom reporters ' +
746 '(https://webdriver.io/docs/customreporter)');
747 }
748 return true;
749 }
750 },
751 /**
752 * set of WDIO services to use
753 */
754 services: {
755 type: 'object',
756 validate: (param) => {
757 /**
758 * should be an array
759 */
760 if (!Array.isArray(param)) {
761 throw new Error('the "services" options needs to be a list of strings and/or arrays');
762 }
763 /**
764 * with arrays and/or strings
765 */
766 for (const option of param) {
767 if (!Array.isArray(option)) {
768 if (typeof option === 'string') {
769 continue;
770 }
771 throw new Error('the "services" options needs to be a list of strings and/or arrays');
772 }
773 }
774 return true;
775 },
776 default: []
777 },
778 /**
779 * Node arguments to specify when launching child processes
780 */
781 execArgv: {
782 type: 'object',
783 validate: (param) => {
784 if (!Array.isArray(param)) {
785 throw new Error('the "execArgv" options needs to be a list of strings');
786 }
787 },
788 default: []
789 },
790 /**
791 * amount of instances to be allowed to run in total
792 */
793 maxInstances: {
794 type: 'number'
795 },
796 /**
797 * amount of instances to be allowed to run per capability
798 */
799 maxInstancesPerCapability: {
800 type: 'number'
801 },
802 /**
803 * whether or not testrunner should inject `browser`, `$` and `$$` as
804 * global environment variables
805 */
806 injectGlobals: {
807 type: 'boolean'
808 },
809 /**
810 * Set to true if you want to update your snapshots.
811 */
812 updateSnapshots: {
813 type: 'string',
814 default: SUPPORTED_SNAPSHOTSTATE_OPTIONS[1],
815 validate: (param) => {
816 if (param && !SUPPORTED_SNAPSHOTSTATE_OPTIONS.includes(param)) {
817 throw new Error(`the "updateSnapshots" options needs to be one of "${SUPPORTED_SNAPSHOTSTATE_OPTIONS.join('", "')}"`);
818 }
819 }
820 },
821 /**
822 * Overrides default snapshot path. For example, to store snapshots next to test files.
823 */
824 resolveSnapshotPath: {
825 type: 'function',
826 validate: (param) => {
827 if (param && typeof param !== 'function') {
828 throw new Error('the "resolveSnapshotPath" options needs to be a function');
829 }
830 }
831 },
832 /**
833 * The number of times to retry the entire specfile when it fails as a whole
834 */
835 specFileRetries: {
836 type: 'number',
837 default: 0
838 },
839 /**
840 * Delay in seconds between the spec file retry attempts
841 */
842 specFileRetriesDelay: {
843 type: 'number',
844 default: 0
845 },
846 /**
847 * Whether or not retried spec files should be retried immediately or deferred to the end of the queue
848 */
849 specFileRetriesDeferred: {
850 type: 'boolean',
851 default: true
852 },
853 /**
854 * whether or not print the log output grouped by test files
855 */
856 groupLogsByTestSpec: {
857 type: 'boolean',
858 default: false
859 },
860 /**
861 * list of strings to watch of `wdio` command is called with `--watch` flag
862 */
863 filesToWatch: {
864 type: 'object',
865 validate: (param) => {
866 if (!Array.isArray(param)) {
867 throw new Error('the "filesToWatch" option needs to be a list of strings');
868 }
869 }
870 },
871 shard: {
872 type: 'object',
873 validate: (param) => {
874 if (typeof param !== 'object') {
875 throw new Error('the "shard" options needs to be an object');
876 }
877 const p = param;
878 if (typeof p.current !== 'number' || typeof p.total !== 'number') {
879 throw new Error('the "shard" option needs to have "current" and "total" properties with number values');
880 }
881 if (p.current < 0 || p.current > p.total) {
882 throw new Error('the "shard.current" value has to be between 0 and "shard.total"');
883 }
884 }
885 },
886 /**
887 * hooks
888 */
889 onPrepare: HOOK_DEFINITION,
890 onWorkerStart: HOOK_DEFINITION,
891 onWorkerEnd: HOOK_DEFINITION,
892 before: HOOK_DEFINITION,
893 beforeSession: HOOK_DEFINITION,
894 beforeSuite: HOOK_DEFINITION,
895 beforeHook: HOOK_DEFINITION,
896 beforeTest: HOOK_DEFINITION,
897 afterTest: HOOK_DEFINITION,
898 afterHook: HOOK_DEFINITION,
899 afterSuite: HOOK_DEFINITION,
900 afterSession: HOOK_DEFINITION,
901 after: HOOK_DEFINITION,
902 onComplete: HOOK_DEFINITION,
903 onReload: HOOK_DEFINITION,
904 beforeAssertion: HOOK_DEFINITION,
905 afterAssertion: HOOK_DEFINITION
906};
907export const WORKER_GROUPLOGS_MESSAGES = {
908 normalExit: (cid) => `\n***** List of steps of WorkerID=[${cid}] *****`,
909 exitWithError: (cid) => `\n***** List of steps of WorkerID=[${cid}] that preceded the error above *****`
910};