import path from 'node:path' import fs from 'node:fs/promises' import inquirer from 'inquirer' import { configHelperSuccessMessage, CONFIG_HELPER_SERENITY_BANNER, SUPPORTED_CONFIG_FILE_EXTENSION, CONFIG_HELPER_INTRO, isNuxtProject, SUPPORTED_PACKAGES } from '../constants.js' import type { ParsedAnswers } from '../types.js' import { createPackageJSON, setupTypeScript, npmInstall, createWDIOConfig, createWDIOScript, runAppiumInstaller, convertPackageHashToObject, getAnswers, getPathForFileGeneration, getProjectProps, getProjectRoot, getSerenityPackages } from '../utils.js' export async function runConfigCommand(parsedAnswers: ParsedAnswers, npmTag: string) { console.log('\n') await createPackageJSON(parsedAnswers) await setupTypeScript(parsedAnswers) await npmInstall(parsedAnswers, npmTag) await createWDIOConfig(parsedAnswers) await createWDIOScript(parsedAnswers) /** * print success message */ console.log( configHelperSuccessMessage({ projectRootDir: parsedAnswers.projectRootDir, runScript: parsedAnswers.serenityAdapter ? 'serenity' : 'wdio', extraInfo: parsedAnswers.serenityAdapter ? CONFIG_HELPER_SERENITY_BANNER : '' }), ) await runAppiumInstaller(parsedAnswers) } /** * Helper utility used in `run` and `install` command to create config if none exist * @param {string} command to be executed by user * @param {string} configPath the path to a wdio.conf.[js/ts] file * @param {Function} runConfigCmd runConfig method to be replaceable for unit testing */ export async function missingConfigurationPrompt(command: string, configPath: string, runConfigCmd: Function = runConfigCommand) { const message = ( `Could not execute "${command}" due to missing configuration, file ` + `"${path.parse(configPath).name}[.js/.ts]" not found! ` + 'Would you like to create one?' ) const { config } = await inquirer.prompt({ type: 'confirm', name: 'config', message, default: false }) /** * don't exit if running unit tests */ if (!config) { /* istanbul ignore next */ console.log(`No WebdriverIO configuration found in "${process.cwd()}"`) /* istanbul ignore next */ if (!process.env.WDIO_UNIT_TESTS) { process.exit(0) } return configPath } const parsedAnswers = await parseAnswers(false) await runConfigCmd(parsedAnswers, 'latest') return configPath } export const parseAnswers = async function (yes: boolean): Promise { console.log(CONFIG_HELPER_INTRO) const answers = await getAnswers(yes) const frameworkPackage = convertPackageHashToObject(answers.framework) const runnerPackage = convertPackageHashToObject(answers.runner || SUPPORTED_PACKAGES.runner[0].value) const servicePackages = answers.services.map((service) => convertPackageHashToObject(service)) const pluginPackages = answers.plugins.map((plugin) => convertPackageHashToObject(plugin)) const serenityPackages = getSerenityPackages(answers) const reporterPackages = answers.reporters.map((reporter) => convertPackageHashToObject(reporter)) const presetPackage = convertPackageHashToObject(answers.preset || '') const projectProps = await getProjectProps(process.cwd()) const projectRootDir = await getProjectRoot(answers) const packagesToInstall: string[] = [ runnerPackage.package, frameworkPackage.package, presetPackage.package, ...reporterPackages.map(reporter => reporter.package), ...pluginPackages.map(plugin => plugin.package), ...servicePackages.map(service => service.package), ...serenityPackages, ].filter(Boolean) /** * find relative paths between tests and pages */ const hasRootTSConfig = await fs.access(path.resolve(projectRootDir, 'tsconfig.json')).then(() => true, () => false) const tsConfigFilePath = !hasRootTSConfig /** * if no tsconfig.json exists in project, create one */ ? path.resolve(projectRootDir, 'tsconfig.json') /** * otherwise make it dependent on whether the user wants to autogenerate files */ : answers.specs /** * if we have autogenerated spec files, put the tsconfig one above the spec file dir */ ? path.resolve( path.dirname(answers.specs.split(path.sep).filter((s) => !s.includes('*')).join(path.sep)), 'tsconfig.json' ) /** * if no spec files are auto generated, create a wdio tsconfig and let the user deal with it */ : path.resolve(projectRootDir, `tsconfig.${runnerPackage.short === 'local' ? 'e2e' : 'wdio'}.json`) const parsedPaths = getPathForFileGeneration(answers, projectRootDir) const isUsingTypeScript = answers.isUsingTypeScript const wdioConfigFilename = `wdio.conf.${isUsingTypeScript ? 'ts' : 'js'}` const wdioConfigPath = path.resolve(projectRootDir, wdioConfigFilename) return { projectName: projectProps?.packageJson.name || 'Test Suite', // default values required in templates ...({ usePageObjects: false, installTestingLibrary: false }), ...answers, useSauceConnect: isNuxtProject || answers.useSauceConnect, rawAnswers: answers, runner: runnerPackage.short as 'local' | 'browser', preset: presetPackage.short, framework: frameworkPackage.short, purpose: runnerPackage.purpose, serenityAdapter: frameworkPackage.package === '@serenity-js/webdriverio' && frameworkPackage.purpose, reporters: reporterPackages.map(({ short }) => short), plugins: pluginPackages.map(({ short }) => short), services: servicePackages.map(({ short }) => short), specs: answers.specs && `./${path.relative(projectRootDir, answers.specs).replaceAll(path.sep, '/')}`, stepDefinitions: answers.stepDefinitions && `./${path.relative(projectRootDir, answers.stepDefinitions).replaceAll(path.sep, '/')}`, packagesToInstall, isUsingTypeScript, esmSupport: projectProps && !(projectProps.esmSupported) ? false : true, isSync: false, _async: 'async ', _await: 'await ', projectRootDir, destSpecRootPath: parsedPaths.destSpecRootPath, destStepRootPath: parsedPaths.destStepRootPath, destPageObjectRootPath: parsedPaths.destPageObjectRootPath, destSerenityLibRootPath: parsedPaths.destSerenityLibRootPath, relativePath: parsedPaths.relativePath, hasRootTSConfig, tsConfigFilePath, tsProject: `./${path.relative(projectRootDir, tsConfigFilePath).replaceAll(path.sep, '/')}`, wdioConfigPath } } /** * Helper utility used in `run` and `install` command to check whether a config file currently exists * @param configPathNoExtension the file path to the WDIO config file without extension * @param configPath the file path to the WDIO config file that is checked first if set * @returns {string} the path to the config file that exists, otherwise undefined */ export async function canAccessConfigPath(configPathNoExtension:string, configPath?:string): Promise { return new Promise((resolve, reject)=>{ if (configPath) { fs.access(configPath).then(()=>resolve(configPath), reject) } else { reject() } }).catch(()=>Promise.all(SUPPORTED_CONFIG_FILE_EXTENSION.map(async (supportedExtension) => { const configPathWithExtension = `${configPathNoExtension}.${supportedExtension}` return fs.access(configPathWithExtension).then(() => configPathWithExtension, () => undefined) })).then( (configFilePaths) => configFilePaths.find(Boolean), () => undefined )) }