UNPKG

@wdio/cli

Version:
910 lines (907 loc) • 39.6 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import { createRequire } from 'node:module'; import { HOOK_DEFINITION } from '@wdio/utils'; import { detectCompiler, getDefaultFiles, convertPackageHashToObject, getProjectRoot, detectPackageManager, } from './utils.js'; const require = createRequire(import.meta.url); export const pkg = require('../package.json'); export const CLI_EPILOGUE = `Documentation: https://webdriver.io\n@wdio/cli (v${pkg.version})`; export const CONFIG_HELPER_INTRO = ` =============================== šŸ¤– WDIO Configuration Wizard šŸ§™ =============================== `; export const PMs = ['npm', 'yarn', 'pnpm', 'bun']; export const SUPPORTED_CONFIG_FILE_EXTENSION = ['js', 'ts', 'mjs', 'mts', 'cjs', 'cts']; export const configHelperSuccessMessage = ({ projectRootDir, runScript, extraInfo = '' }) => ` šŸ¤– Successfully setup project at ${projectRootDir} šŸŽ‰ Join our Discord Community Server and instantly find answers to your issues or queries. Or just join and say hi šŸ‘‹! šŸ”— https://discord.webdriver.io Visit the project on GitHub to report bugs šŸ› or raise feature requests šŸ’”: šŸ”— https://github.com/webdriverio/webdriverio ${extraInfo} To run your tests, execute: $ cd ${projectRootDir} $ npm run ${runScript} `; export const CONFIG_HELPER_SERENITY_BANNER = ` Learn more about Serenity/JS: šŸ”— https://serenity-js.org `; export const DEPENDENCIES_INSTALLATION_MESSAGE = ` To install dependencies, execute: %s `; export const NPM_INSTALL = ''; export const ANDROID_CONFIG = { platformName: 'Android', automationName: 'UiAutomator2', deviceName: 'Test' }; export const IOS_CONFIG = { platformName: 'iOS', automationName: 'XCUITest', deviceName: 'iPhone Simulator' }; export var CompilerOptions; (function (CompilerOptions) { CompilerOptions["Babel"] = "Babel (https://babeljs.io/)"; CompilerOptions["TS"] = "TypeScript (https://www.typescriptlang.org/)"; CompilerOptions["Nil"] = "No!"; })(CompilerOptions || (CompilerOptions = {})); /** * We have to use a string hash for value because InquirerJS default values do not work if we have * objects as a `value` to be stored from the user's answers. */ export const SUPPORTED_PACKAGES = { runner: [ { name: 'E2E Testing - of Web or Mobile Applications', value: '@wdio/local-runner$--$local$--$e2e' }, { name: 'Component or Unit Testing - in the browser\n > https://webdriver.io/docs/component-testing', value: '@wdio/browser-runner$--$browser$--$component' }, { name: 'Desktop Testing - of Electron Applications\n > https://webdriver.io/docs/desktop-testing/electron', value: '@wdio/local-runner$--$local$--$electron' }, { name: 'Desktop Testing - of MacOS Applications\n > https://webdriver.io/docs/desktop-testing/macos', value: '@wdio/local-runner$--$local$--$macos' }, { name: 'VS Code Extension Testing\n > https://webdriver.io/docs/vscode-extension-testing', value: '@wdio/local-runner$--$local$--$vscode' } ], framework: [ { name: 'Mocha (https://mochajs.org/)', value: '@wdio/mocha-framework$--$mocha' }, { name: 'Mocha with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha' }, { name: 'Jasmine (https://jasmine.github.io/)', value: '@wdio/jasmine-framework$--$jasmine' }, { name: 'Jasmine with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine' }, { name: 'Cucumber (https://cucumber.io/)', value: '@wdio/cucumber-framework$--$cucumber' }, { name: 'Cucumber with Serenity/JS (https://serenity-js.org/)', value: '@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber' }, ], reporter: [ { name: 'spec', value: '@wdio/spec-reporter$--$spec' }, { name: 'dot', value: '@wdio/dot-reporter$--$dot' }, { name: 'junit', value: '@wdio/junit-reporter$--$junit' }, { name: 'allure', value: '@wdio/allure-reporter$--$allure' }, { name: 'sumologic', value: '@wdio/sumologic-reporter$--$sumologic' }, { name: 'concise', value: '@wdio/concise-reporter$--$concise' }, { name: 'json', value: '@wdio/json-reporter$--$json' }, // external { name: 'reportportal', value: 'wdio-reportportal-reporter$--$reportportal' }, { name: 'video', value: 'wdio-video-reporter$--$video' }, { name: 'cucumber-json', value: 'wdio-cucumberjs-json-reporter$--$cucumberjs-json' }, { name: 'mochawesome', value: 'wdio-mochawesome-reporter$--$mochawesome' }, { name: 'timeline', value: 'wdio-timeline-reporter$--$timeline' }, { name: 'html-nice', value: 'wdio-html-nice-reporter$--$html-nice' }, { name: 'slack', value: '@moroo/wdio-slack-reporter$--$slack' }, { name: 'teamcity', value: 'wdio-teamcity-reporter$--$teamcity' }, { name: 'delta', value: '@delta-reporter/wdio-delta-reporter-service$--$delta' }, { name: 'testrail', value: '@wdio/testrail-reporter$--$testrail' }, { name: 'light', value: 'wdio-light-reporter$--$light' } ], plugin: [ { 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' }, { name: 'angular-component-harnesses: support for Angular component test harnesses\n > https://www.npmjs.com/package/@badisi/wdio-harness', value: '@badisi/wdio-harness$--$harness' }, { 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' } ], service: [ // internal or community driver services { name: 'visual', value: '@wdio/visual-service$--$visual' }, { name: 'vite', value: 'wdio-vite-service$--$vite' }, { name: 'nuxt', value: 'wdio-nuxt-service$--$nuxt' }, { name: 'firefox-profile', value: '@wdio/firefox-profile-service$--$firefox-profile' }, { name: 'gmail', value: 'wdio-gmail-service$--$gmail' }, { name: 'sauce', value: '@wdio/sauce-service$--$sauce' }, { name: 'testingbot', value: '@wdio/testingbot-service$--$testingbot' }, { name: 'crossbrowsertesting', value: '@wdio/crossbrowsertesting-service$--$crossbrowsertesting' }, { name: 'browserstack', value: '@wdio/browserstack-service$--$browserstack' }, { name: 'devtools', value: '@wdio/devtools-service$--$devtools' }, { name: 'vscode', value: 'wdio-vscode-service$--$vscode' }, { name: 'electron', value: 'wdio-electron-service$--$electron' }, { name: 'appium', value: '@wdio/appium-service$--$appium' }, // external { name: 'eslinter-service', value: 'wdio-eslinter-service$--$eslinter' }, { name: 'lambdatest', value: 'wdio-lambdatest-service$--$lambdatest' }, { name: 'zafira-listener', value: 'wdio-zafira-listener-service$--$zafira-listener' }, { name: 'reportportal', value: 'wdio-reportportal-service$--$reportportal' }, { name: 'docker', value: 'wdio-docker-service$--$docker' }, { name: 'ui5', value: 'wdio-ui5-service$--$ui5' }, { name: 'wiremock', value: 'wdio-wiremock-service$--$wiremock' }, { name: 'ng-apimock', value: 'wdio-ng-apimock-service$--$ng-apimock' }, { name: 'slack', value: 'wdio-slack-service$--$slack' }, { name: 'cucumber-viewport-logger', value: 'wdio-cucumber-viewport-logger-service$--$cucumber-viewport-logger' }, { name: 'intercept', value: 'wdio-intercept-service$--$intercept' }, { name: 'docker', value: 'wdio-docker-service$--$docker' }, { name: 'novus-visual-regression', value: 'wdio-novus-visual-regression-service$--$novus-visual-regression' }, { name: 'rerun', value: 'wdio-rerun-service$--$rerun' }, { name: 'winappdriver', value: 'wdio-winappdriver-service$--$winappdriver' }, { name: 'ywinappdriver', value: 'wdio-ywinappdriver-service$--$ywinappdriver' }, { name: 'performancetotal', value: 'wdio-performancetotal-service$--$performancetotal' }, { name: 'cleanuptotal', value: 'wdio-cleanuptotal-service$--$cleanuptotal' }, { name: 'aws-device-farm', value: 'wdio-aws-device-farm-service$--$aws-device-farm' }, { name: 'ms-teams', value: 'wdio-ms-teams-service$--$ms-teams' }, { name: 'tesults', value: 'wdio-tesults-service$--$tesults' }, { name: 'azure-devops', value: '@gmangiapelo/wdio-azure-devops-service$--$azure-devops' }, { name: 'google-Chat', value: 'wdio-google-chat-service$--$google-chat' }, { name: 'qmate-service', value: '@sap_oss/wdio-qmate-service$--$qmate-service' }, { name: 'vitaqai', value: 'wdio-vitaqai-service$--$vitaqai' }, { name: 'robonut', value: 'wdio-robonut-service$--$robonut' }, { name: 'qunit', value: 'wdio-qunit-service$--$qunit' } ] }; export const SUPPORTED_BROWSER_RUNNER_PRESETS = [ { name: 'Lit (https://lit.dev/)', value: '$--$' }, { name: 'Vue.js (https://vuejs.org/)', value: '@vitejs/plugin-vue$--$vue' }, { name: 'Svelte (https://svelte.dev/)', value: '@sveltejs/vite-plugin-svelte$--$svelte' }, { name: 'SolidJS (https://www.solidjs.com/)', value: 'vite-plugin-solid$--$solid' }, { name: 'StencilJS (https://stenciljs.com/)', value: '$--$stencil' }, { name: 'React (https://reactjs.org/)', value: '@vitejs/plugin-react$--$react' }, { name: 'Preact (https://preactjs.com/)', value: '@preact/preset-vite$--$preact' }, { name: 'Other', value: false } ]; export const TESTING_LIBRARY_PACKAGES = { react: '@testing-library/react', preact: '@testing-library/preact', vue: '@testing-library/vue', svelte: '@testing-library/svelte', solid: 'solid-testing-library' }; export var BackendChoice; (function (BackendChoice) { BackendChoice["Local"] = "On my local machine"; BackendChoice["Experitest"] = "In the cloud using Experitest"; BackendChoice["Saucelabs"] = "In the cloud using Sauce Labs"; BackendChoice["Browserstack"] = "In the cloud using BrowserStack"; BackendChoice["OtherVendors"] = "In the cloud using Testingbot or LambdaTest or a different service"; BackendChoice["Grid"] = "I have my own Selenium cloud"; })(BackendChoice || (BackendChoice = {})); export var ElectronBuildToolChoice; (function (ElectronBuildToolChoice) { ElectronBuildToolChoice["ElectronForge"] = "Electron Forge (https://www.electronforge.io/)"; ElectronBuildToolChoice["ElectronBuilder"] = "electron-builder (https://www.electron.build/)"; ElectronBuildToolChoice["SomethingElse"] = "Something else"; })(ElectronBuildToolChoice || (ElectronBuildToolChoice = {})); var ProtocolOptions; (function (ProtocolOptions) { ProtocolOptions["HTTPS"] = "https"; ProtocolOptions["HTTP"] = "http"; })(ProtocolOptions || (ProtocolOptions = {})); export var RegionOptions; (function (RegionOptions) { RegionOptions["US"] = "us"; RegionOptions["EU"] = "eu"; RegionOptions["APAC"] = "apac"; })(RegionOptions || (RegionOptions = {})); export const E2E_ENVIRONMENTS = [ { name: 'Web - web applications in the browser', value: 'web' }, { name: 'Mobile - native, hybrid and mobile web apps, on Android or iOS', value: 'mobile' } ]; export const MOBILE_ENVIRONMENTS = [ { 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' }, { name: 'iOS - applications on iOS, iPadOS, and tvOS\n > using XCTest (https://appium.github.io/appium-xcuitest-driver)', value: 'ios' } ]; export const BROWSER_ENVIRONMENTS = [ { name: 'Chrome', value: 'chrome' }, { name: 'Firefox', value: 'firefox' }, { name: 'Safari', value: 'safari' }, { name: 'Microsoft Edge', value: 'MicrosoftEdge' } ]; function isBrowserRunner(answers) { return answers.runner === SUPPORTED_PACKAGES.runner[1].value; } export function usesSerenity(answers) { return answers.framework.includes('serenity-js'); } function getTestingPurpose(answers) { return convertPackageHashToObject(answers.runner).purpose; } export const isNuxtProject = await Promise.all([ path.join(process.cwd(), 'nuxt.config.js'), path.join(process.cwd(), 'nuxt.config.ts'), path.join(process.cwd(), 'nuxt.config.mjs'), path.join(process.cwd(), 'nuxt.config.mts') ].map((p) => fs.access(p).then(() => true, () => false))).then((res) => res.some(Boolean), () => false); function selectDefaultService(serviceNames) { serviceNames = Array.isArray(serviceNames) ? serviceNames : [serviceNames]; return SUPPORTED_PACKAGES.service /* istanbul ignore next */ .filter(({ name }) => serviceNames.includes(name)) .map(({ value }) => value); } function prioServiceOrderFor(serviceNamesParam) { const serviceNames = Array.isArray(serviceNamesParam) ? serviceNamesParam : [serviceNamesParam]; let services = SUPPORTED_PACKAGES.service; for (const serviceName of serviceNames) { const index = services.findIndex(({ name }) => name === serviceName); services = [services[index], ...services.slice(0, index), ...services.slice(index + 1)]; } return services; } export const QUESTIONNAIRE = [{ type: 'list', name: 'runner', message: 'What type of testing would you like to do?', choices: SUPPORTED_PACKAGES.runner }, { type: 'list', name: 'preset', message: 'Which framework do you use for building components?', choices: SUPPORTED_BROWSER_RUNNER_PRESETS, // only ask if there are more than 1 runner to pick from when: /* istanbul ignore next */ isBrowserRunner }, { type: 'confirm', name: 'installTestingLibrary', message: 'Do you like to use Testing Library (https://testing-library.com/) as test utility?', default: true, // only ask if there are more than 1 runner to pick from when: /* istanbul ignore next */ (answers) => (isBrowserRunner(answers) && /** * Only show if Testing Library has an add-on for framework */ answers.preset && TESTING_LIBRARY_PACKAGES[convertPackageHashToObject(answers.preset).short]) }, { type: 'list', name: 'electronBuildTool', message: 'Which tool are you using to build your Electron app?', choices: Object.values(ElectronBuildToolChoice), when: /* instanbul ignore next */ (answers) => getTestingPurpose(answers) === 'electron' }, { type: 'input', name: 'electronAppBinaryPath', message: 'What is the path to the binary of your built Electron app?', when: /* istanbul ignore next */ (answers) => getTestingPurpose(answers) === 'electron' && (answers.electronBuildTool === ElectronBuildToolChoice.SomethingElse) }, { type: 'list', name: 'backend', message: 'Where is your automation backend located?', choices: Object.values(BackendChoice), when: /* instanbul ignore next */ (answers) => getTestingPurpose(answers) === 'e2e' }, { type: 'list', name: 'e2eEnvironment', message: 'Which environment you would like to automate?', choices: E2E_ENVIRONMENTS, default: 'web', when: /* istanbul ignore next */ (answers) => getTestingPurpose(answers) === 'e2e' }, { type: 'list', name: 'mobileEnvironment', message: 'Which mobile environment you\'ld like to automate?', choices: MOBILE_ENVIRONMENTS, when: /* instanbul ignore next */ (answers) => (getTestingPurpose(answers) === 'e2e' && answers.e2eEnvironment === 'mobile') }, { type: 'checkbox', name: 'browserEnvironment', message: 'With which browser should we start?', choices: BROWSER_ENVIRONMENTS, default: ['chrome'], when: /* instanbul ignore next */ (answers) => (getTestingPurpose(answers) === 'e2e' && answers.e2eEnvironment === 'web') }, { type: 'input', name: 'hostname', message: 'What is the host address of that cloud service?', when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.indexOf('different service') > -1 }, { type: 'input', name: 'port', message: 'What is the port on which that service is running?', default: '80', when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.indexOf('different service') > -1 }, { type: 'input', name: 'expEnvAccessKey', message: 'Access key from Experitest Cloud', default: 'EXPERITEST_ACCESS_KEY', when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Experitest }, { type: 'input', name: 'expEnvHostname', message: 'Environment variable for cloud url', default: 'example.experitest.com', when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Experitest }, { type: 'input', name: 'expEnvPort', message: 'Environment variable for port', default: '443', when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Experitest }, { type: 'list', name: 'expEnvProtocol', message: 'Choose a protocol for environment variable', default: ProtocolOptions.HTTPS, choices: Object.values(ProtocolOptions), when: /* istanbul ignore next */ (answers) => (answers.backend === BackendChoice.Experitest && answers.expEnvPort !== '80' && answers.expEnvPort !== '443') }, { type: 'input', name: 'env_user', message: 'Environment variable for username', default: 'LT_USERNAME', when: /* istanbul ignore next */ (answers) => (answers.backend && answers.backend.indexOf('LambdaTest') > -1 && answers.hostname.indexOf('lambdatest.com') > -1) }, { type: 'input', name: 'env_key', message: 'Environment variable for access key', default: 'LT_ACCESS_KEY', when: /* istanbul ignore next */ (answers) => (answers.backend && answers.backend.indexOf('LambdaTest') > -1 && answers.hostname.indexOf('lambdatest.com') > -1) }, { type: 'input', name: 'env_user', message: 'Environment variable for username', default: 'BROWSERSTACK_USERNAME', when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Browserstack }, { type: 'input', name: 'env_key', message: 'Environment variable for access key', default: 'BROWSERSTACK_ACCESS_KEY', when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Browserstack }, { type: 'input', name: 'env_user', message: 'Environment variable for username', default: 'SAUCE_USERNAME', when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Saucelabs }, { type: 'input', name: 'env_key', message: 'Environment variable for access key', default: 'SAUCE_ACCESS_KEY', when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Saucelabs }, { type: 'list', name: 'region', message: 'In which region do you want to run your Sauce Labs tests in?', choices: Object.values(RegionOptions), when: /* istanbul ignore next */ (answers) => answers.backend === BackendChoice.Saucelabs }, { type: 'confirm', name: 'useSauceConnect', message: ('Are you testing a local application and need Sauce Connect to be set-up?\n' + 'Read more on Sauce Connect at: https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy'), default: isNuxtProject, when: /* istanbul ignore next */ (answers) => (answers.backend === BackendChoice.Saucelabs && !isNuxtProject) }, { type: 'input', name: 'hostname', message: 'What is the IP or URI to your Selenium standalone or grid server?', default: 'localhost', when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1 }, { type: 'input', name: 'port', message: 'What is the port which your Selenium standalone or grid server is running on?', default: '4444', when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1 }, { type: 'input', name: 'path', message: 'What is the path to your browser driver or grid server?', default: '/', when: /* istanbul ignore next */ (answers) => answers.backend && answers.backend.toString().indexOf('own Selenium cloud') > -1 }, { type: 'list', name: 'framework', message: 'Which framework do you want to use?', choices: /* instanbul ignore next */ (answers) => { /** * browser runner currently supports only Mocha framework */ if (isBrowserRunner(answers)) { return SUPPORTED_PACKAGES.framework.slice(0, 1); } /** * Serenity tests don't come with proper ElectronJS example files */ if (getTestingPurpose(answers) === 'electron') { return SUPPORTED_PACKAGES.framework.filter(({ value }) => !value.startsWith('@serenity-js')); } return SUPPORTED_PACKAGES.framework; } }, { type: 'list', name: 'isUsingCompiler', message: 'Do you want to use a compiler?', choices: (answers) => { /** * StencilJS only supports TypeScript */ if (answers.preset && answers.preset.includes('stencil')) { return [CompilerOptions.TS]; } return Object.values(CompilerOptions); }, default: /* istanbul ignore next */ (answers) => detectCompiler(answers) }, { type: 'confirm', name: 'generateTestFiles', message: 'Do you want WebdriverIO to autogenerate some test files?', default: true, when: /* istanbul ignore next */ (answers) => { /** * we only have examples for Mocha and Jasmine */ if (['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) && answers.framework.includes('cucumber')) { return false; } return true; } }, { type: 'input', name: 'specs', message: 'What should be the location of your spec files?', default: /* istanbul ignore next */ (answers) => { const pattern = isBrowserRunner(answers) ? 'src/**/*.test' : 'test/specs/**/*'; return getDefaultFiles(answers, pattern); }, when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.framework.match(/(mocha|jasmine)/) }, { type: 'input', name: 'specs', message: 'What should be the location of your feature files?', default: (answers) => getDefaultFiles(answers, 'features/**/*.feature'), when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.framework.includes('cucumber') }, { type: 'input', name: 'stepDefinitions', message: 'What should be the location of your step definitions?', default: (answers) => getDefaultFiles(answers, 'features/step-definitions/steps'), when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.framework.includes('cucumber') }, { type: 'confirm', name: 'usePageObjects', message: 'Do you want to use page objects (https://martinfowler.com/bliki/PageObject.html)?', default: true, when: /* istanbul ignore next */ (answers) => (answers.generateTestFiles && /** * page objects aren't common for component testing */ !isBrowserRunner(answers) && /** * and also not needed when running VS Code tests since the service comes with * its own page object implementation, nor when running Electron or MacOS tests */ !['vscode', 'electron', 'macos'].includes(getTestingPurpose(answers)) && /** * Serenity/JS generates Lean Page Objects by default, so there's no need to ask about it * See https://serenity-js.org/handbook/web-testing/page-objects-pattern/ */ !usesSerenity(answers)) }, { type: 'input', name: 'pages', message: 'Where are your page objects located?', default: /* istanbul ignore next */ (answers) => (answers.framework.match(/(mocha|jasmine)/) ? getDefaultFiles(answers, 'test/pageobjects/**/*') : getDefaultFiles(answers, 'features/pageobjects/**/*')), when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && answers.usePageObjects }, { type: 'input', name: 'serenityLibPath', message: 'What should be the location of your Serenity/JS Screenplay Pattern library?', default: /* istanbul ignore next */ async (answers) => { const projectRootDir = await getProjectRoot(answers); const specsDir = path.resolve(projectRootDir, path.dirname(answers.specs || '').replace(/\*\*$/, '')); return path.resolve(specsDir, '..', 'serenity'); }, when: /* istanbul ignore next */ (answers) => answers.generateTestFiles && usesSerenity(answers) }, { type: 'checkbox', name: 'reporters', message: 'Which reporter do you want to use?', choices: SUPPORTED_PACKAGES.reporter, // @ts-ignore default: [SUPPORTED_PACKAGES.reporter.find( /* istanbul ignore next */ ({ name }) => name === 'spec').value ] }, { type: 'checkbox', name: 'plugins', message: 'Do you want to add a plugin to your test setup?', choices: SUPPORTED_PACKAGES.plugin, default: [] }, { type: 'confirm', name: 'includeVisualTesting', message: 'Would you like to include Visual Testing to your setup? For more information see https://webdriver.io/docs/visual-testing!', default: false, when: /* istanbul ignore next */ (answers) => { /** * visual testing mostly makes sense for e2e and component tests */ return ['e2e', 'component'].includes(getTestingPurpose(answers)); } }, { type: 'checkbox', name: 'services', message: 'Do you want to add a service to your test setup?', choices: (answers) => { const services = []; if (answers.backend === BackendChoice.Browserstack) { services.push('browserstack'); } else if (answers.backend === BackendChoice.Saucelabs) { services.push('sauce'); } if (answers.e2eEnvironment === 'mobile') { services.push('appium'); } if (getTestingPurpose(answers) === 'e2e' && isNuxtProject) { services.push('nuxt'); } if (getTestingPurpose(answers) === 'vscode') { return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'vscode')]; } else if (getTestingPurpose(answers) === 'electron') { return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'electron')]; } else if (getTestingPurpose(answers) === 'macos') { return [SUPPORTED_PACKAGES.service.find(({ name }) => name === 'appium')]; } return prioServiceOrderFor(services); }, default: (answers) => { const defaultServices = []; if (answers.backend === BackendChoice.Browserstack) { defaultServices.push('browserstack'); } else if (answers.backend === BackendChoice.Saucelabs) { defaultServices.push('sauce'); } if (answers.e2eEnvironment === 'mobile' || getTestingPurpose(answers) === 'macos') { defaultServices.push('appium'); } if (getTestingPurpose(answers) === 'vscode') { defaultServices.push('vscode'); } else if (getTestingPurpose(answers) === 'electron') { defaultServices.push('electron'); } if (isNuxtProject) { defaultServices.push('nuxt'); } if (answers.includeVisualTesting) { defaultServices.push('visual'); } return selectDefaultService(defaultServices); } }, { type: 'input', name: 'outputDir', message: 'In which directory should the xunit reports get stored?', default: './', when: /* istanbul ignore next */ (answers) => answers.reporters.includes('junit') }, { type: 'input', name: 'outputDir', message: 'In which directory should the json reports get stored?', default: './', when: /* istanbul ignore next */ (answers) => answers.reporters.includes('json') }, { type: 'input', name: 'outputDir', message: 'In which directory should the mochawesome json reports get stored?', default: './', when: /* istanbul ignore next */ (answers) => answers.reporters.includes('mochawesome') }, { type: 'confirm', name: 'npmInstall', message: () => `Do you want me to run \`${detectPackageManager()} install\``, default: true }]; const SUPPORTED_SNAPSHOTSTATE_OPTIONS = ['all', 'new', 'none']; export const COMMUNITY_PACKAGES_WITH_TS_SUPPORT = [ 'wdio-electron-service', 'wdio-vscode-service', 'wdio-nuxt-service', 'wdio-vite-service', 'wdio-gmail-service' ]; export const TESTRUNNER_DEFAULTS = { /** * Define specs for test execution. You can either specify a glob * pattern to match multiple files at once or wrap a glob or set of * paths into an array to run them within a single worker process. */ specs: { type: 'object', validate: (param) => { if (!Array.isArray(param)) { throw new Error('the "specs" option needs to be a list of strings'); } } }, /** * exclude specs from test execution */ exclude: { type: 'object', validate: (param) => { if (!Array.isArray(param)) { throw new Error('the "exclude" option needs to be a list of strings'); } } }, /** * key/value definition of suites (named by key) and a list of specs as value * to specify a specific set of tests to execute */ suites: { type: 'object' }, /** * Project root directory path. */ rootDir: { type: 'string' }, /** * If you only want to run your tests until a specific amount of tests have failed use * bail (default is 0 - don't bail, run all tests). */ bail: { type: 'number', default: 0 }, /** * supported test framework by wdio testrunner */ framework: { type: 'string' }, /** * capabilities of WebDriver sessions */ capabilities: { type: 'object', validate: (param) => { /** * should be an object */ if (!Array.isArray(param)) { if (typeof param === 'object') { return true; } throw new Error('the "capabilities" options needs to be an object or a list of objects'); } /** * or an array of objects */ for (const option of param) { if (typeof option === 'object') { // Check does not work recursively continue; } throw new Error('expected every item of a list of capabilities to be of type object'); } return true; }, required: true }, /** * list of reporters to use, a reporter can be either a string or an object with * reporter options, e.g.: * [ * 'dot', * { * name: 'spec', * outputDir: __dirname + '/reports' * } * ] */ reporters: { type: 'object', validate: (param) => { /** * option must be an array */ if (!Array.isArray(param)) { throw new Error('the "reporters" options needs to be a list of strings'); } const isValidReporter = (option) => ((typeof option === 'string') || (typeof option === 'function')); /** * array elements must be: */ for (const option of param) { /** * either a string or a function (custom reporter) */ if (isValidReporter(option)) { continue; } /** * or an array with the name of the reporter as first element and the options * as second element */ if (Array.isArray(option) && typeof option[1] === 'object' && isValidReporter(option[0])) { continue; } throw new Error('a reporter should be either a string in the format "wdio-<reportername>-reporter" ' + 'or a function/class. Please see the docs for more information on custom reporters ' + '(https://webdriver.io/docs/customreporter)'); } return true; } }, /** * set of WDIO services to use */ services: { type: 'object', validate: (param) => { /** * should be an array */ if (!Array.isArray(param)) { throw new Error('the "services" options needs to be a list of strings and/or arrays'); } /** * with arrays and/or strings */ for (const option of param) { if (!Array.isArray(option)) { if (typeof option === 'string') { continue; } throw new Error('the "services" options needs to be a list of strings and/or arrays'); } } return true; }, default: [] }, /** * Node arguments to specify when launching child processes */ execArgv: { type: 'object', validate: (param) => { if (!Array.isArray(param)) { throw new Error('the "execArgv" options needs to be a list of strings'); } }, default: [] }, /** * amount of instances to be allowed to run in total */ maxInstances: { type: 'number' }, /** * amount of instances to be allowed to run per capability */ maxInstancesPerCapability: { type: 'number' }, /** * whether or not testrunner should inject `browser`, `$` and `$$` as * global environment variables */ injectGlobals: { type: 'boolean' }, /** * Set to true if you want to update your snapshots. */ updateSnapshots: { type: 'string', default: SUPPORTED_SNAPSHOTSTATE_OPTIONS[1], validate: (param) => { if (param && !SUPPORTED_SNAPSHOTSTATE_OPTIONS.includes(param)) { throw new Error(`the "updateSnapshots" options needs to be one of "${SUPPORTED_SNAPSHOTSTATE_OPTIONS.join('", "')}"`); } } }, /** * Overrides default snapshot path. For example, to store snapshots next to test files. */ resolveSnapshotPath: { type: 'function', validate: (param) => { if (param && typeof param !== 'function') { throw new Error('the "resolveSnapshotPath" options needs to be a function'); } } }, /** * The number of times to retry the entire specfile when it fails as a whole */ specFileRetries: { type: 'number', default: 0 }, /** * Delay in seconds between the spec file retry attempts */ specFileRetriesDelay: { type: 'number', default: 0 }, /** * Whether or not retried spec files should be retried immediately or deferred to the end of the queue */ specFileRetriesDeferred: { type: 'boolean', default: true }, /** * whether or not print the log output grouped by test files */ groupLogsByTestSpec: { type: 'boolean', default: false }, /** * list of strings to watch of `wdio` command is called with `--watch` flag */ filesToWatch: { type: 'object', validate: (param) => { if (!Array.isArray(param)) { throw new Error('the "filesToWatch" option needs to be a list of strings'); } } }, shard: { type: 'object', validate: (param) => { if (typeof param !== 'object') { throw new Error('the "shard" options needs to be an object'); } const p = param; if (typeof p.current !== 'number' || typeof p.total !== 'number') { throw new Error('the "shard" option needs to have "current" and "total" properties with number values'); } if (p.current < 0 || p.current > p.total) { throw new Error('the "shard.current" value has to be between 0 and "shard.total"'); } } }, /** * hooks */ onPrepare: HOOK_DEFINITION, onWorkerStart: HOOK_DEFINITION, onWorkerEnd: HOOK_DEFINITION, before: HOOK_DEFINITION, beforeSession: HOOK_DEFINITION, beforeSuite: HOOK_DEFINITION, beforeHook: HOOK_DEFINITION, beforeTest: HOOK_DEFINITION, afterTest: HOOK_DEFINITION, afterHook: HOOK_DEFINITION, afterSuite: HOOK_DEFINITION, afterSession: HOOK_DEFINITION, after: HOOK_DEFINITION, onComplete: HOOK_DEFINITION, onReload: HOOK_DEFINITION, beforeAssertion: HOOK_DEFINITION, afterAssertion: HOOK_DEFINITION }; export const WORKER_GROUPLOGS_MESSAGES = { normalExit: (cid) => `\n***** List of steps of WorkerID=[${cid}] *****`, exitWithError: (cid) => `\n***** List of steps of WorkerID=[${cid}] that preceded the error above *****` };