import url from 'node:url' import path from 'node:path' import util, { promisify } from 'node:util' import fs from 'node:fs/promises' import { execSync, } from 'node:child_process' import readDir from 'recursive-readdir' import { $ } from 'execa' import { readPackageUp } from 'read-pkg-up' import { EjsHelpers } from './templates/EjsHelpers.js' import inquirer from 'inquirer' import ejs from 'ejs' import type { SpawnOptions } from 'node:child_process' import spawn from 'cross-spawn' import { COMMUNITY_PACKAGES_WITH_TS_SUPPORT, DEPENDENCIES_INSTALLATION_MESSAGE, pkg, SUPPORTED_PACKAGE_MANAGERS, QUESTIONNAIRE, TESTING_LIBRARY_PACKAGES, usesSerenity } from './constants.js' import type { ParsedAnswers, ProjectProps, Questionnair, SupportedPackage } from './types.js' import chalk from 'chalk' import { getInstallCommand, installPackages } from './install.js' const NPM_COMMAND = /^win/.test(process.platform) ? 'npm.cmd' : 'npm' const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) process.on('SIGINT', () => printAndExit(undefined, 'SIGINT')) /** * to resolve properly during WDIO unit tests we need resolve the template root dir * based on the VITEST_WORKER_ID environment variable */ const TEMPLATE_ROOT_DIR = process.env.WDIO_UNIT_TESTS ? path.join(__dirname, 'templates', 'exampleFiles') : path.join(__dirname, '..', 'templates', 'exampleFiles') export function runProgram (command: string, args: string[], options: SpawnOptions) { const child = spawn(command, args, { stdio: 'inherit', ...options }) return new Promise((resolve, rejects) => { let error: Error child.on('error', (e) => (error = e)) child.on('close', (code, signal) => { if (code !== 0) { const errorMessage = (error && error.message) || `Error calling: ${command} ${args.join(' ')}` printAndExit(errorMessage, signal) return rejects(errorMessage) } resolve() }) }) } export async function getPackageVersion() { try { const pkgJsonPath = path.join(__dirname, '..', 'package.json') const pkg = JSON.parse((await fs.readFile(pkgJsonPath)).toString()) return `v${pkg.version}` } catch { /* ignore */ } return 'unknown' } function printAndExit (error?: string, signal?: NodeJS.Signals | null) { if (signal === 'SIGINT') { console.log('\n\nGoodbye šŸ‘‹') } else { console.log(`\n\nāš ļø Ups, something went wrong${error ? `: ${error}` : ''}!`) } process.exit(1) } // transported from cli export function convertPackageHashToObject(pkg: string, hash = '$--$'): SupportedPackage { const [p, short, purpose] = pkg.split(hash) return { package: p, short, purpose } } export async function getAnswers(yes: boolean): Promise { if (yes) { const ignoredQuestions = ['e2eEnvironment'] const filteredQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name)) const answers = {} as Questionnair for (const question of filteredQuestionaire) { /** * set nothing if question doesn't apply */ if (question.when && !question.when(answers)) { continue } Object.assign(answers, { [question.name]: typeof question.default !== 'undefined' /** * set default value if existing */ ? typeof question.default === 'function' ? await question.default(answers) : question.default : question.choices && question.choices.length /** * pick first choice, select value if it exists */ ? typeof question.choices === 'function' ? (question.choices(answers)[0] as unknown as { value: unknown }).value ? (question.choices(answers)[0] as unknown as { value: unknown }).value : question.choices(answers)[0] : (question.choices[0] as { value: unknown }).value ? question.type === 'checkbox' ? [(question.choices[0] as { value: unknown }).value] : (question.choices[0] as { value: unknown }).value : question.choices[0] : {} }) } /** * some questions have async defaults */ answers.isUsingTypeScript = await answers.isUsingTypeScript answers.specs = await answers.specs answers.pages = await answers.pages return answers } const projectProps = await getProjectProps(process.cwd()) const isProjectExisting = Boolean(projectProps) const nameInPackageJsonIsNotCreateWdioDefault = projectProps?.packageJson?.name !== 'my-new-project' const projectName = projectProps?.packageJson?.name ? ` named "${projectProps.packageJson.name}"` : '' const projectRootQuestions = [ { type: 'confirm', name: 'createPackageJSON', default: true, message: `Couldn't find a package.json in "${process.cwd()}" or any of the parent directories, do you want to create one?`, when: !isProjectExisting }, { type: 'confirm', name: 'projectRootCorrect', default: true, message: `A project${projectName} was detected at "${projectProps?.path}", correct?`, when: isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault }, { type: 'input', name: 'projectRoot', message: 'What is the project root for your test project?', default: projectProps?.path, // only ask if there are more than 1 runner to pick from when: /* istanbul ignore next */ (answers: Questionnair) => isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault && !answers.projectRootCorrect } ] // @ts-ignore const answers = await inquirer.prompt(projectRootQuestions) if (answers.createPackageJSON) { answers.projectRoot = process.cwd() } // @ts-ignore return inquirer.prompt(QUESTIONNAIRE, answers) } /** * Receive project properties * @returns {@type ProjectProps} if a package.json can be found in cwd or parent directories, otherwise undefined * which means that a new project can be created */ export async function getProjectProps(cwd = process.cwd()): Promise { try { const { packageJson, path: packageJsonPath } = await readPackageUp({ cwd }) || {} if (!packageJson || !packageJsonPath) { return undefined } return { esmSupported: ( packageJson.type === 'module' || typeof packageJson.module === 'string' ), packageJson, path: path.dirname(packageJsonPath) } } catch { return undefined } } export function getPathForFileGeneration(answers: Questionnair, projectRootDir: string) { const specAnswer = answers.specs || '' const stepDefinitionAnswer = answers.stepDefinitions || '' const pageObjectAnswer = answers.pages || '' const destSpecRootPath = generatePathFromAnswer(specAnswer, projectRootDir).replace(/\*\*$/, '') const destStepRootPath = generatePathFromAnswer(stepDefinitionAnswer, projectRootDir) const destPageObjectRootPath = answers.usePageObjects ? generatePathFromAnswer(pageObjectAnswer, projectRootDir).replace(/\*\*$/, '') : '' const destSerenityLibRootPath = usesSerenity(answers) ? path.resolve(projectRootDir, answers.serenityLibPath || 'serenity') : '' const relativePath = (answers.generateTestFiles && answers.usePageObjects) ? !(convertPackageHashToObject(answers.framework).short === 'cucumber') ? path.relative(destSpecRootPath, destPageObjectRootPath) : path.relative(destStepRootPath, destPageObjectRootPath) : '' return { destSpecRootPath: destSpecRootPath, destStepRootPath: destStepRootPath, destPageObjectRootPath: destPageObjectRootPath, destSerenityLibRootPath: destSerenityLibRootPath, relativePath: relativePath.replaceAll(path.sep, '/') } } /** * Generates a valid file path from answers provided. * @param answers The answer from which a file path is to be generated. * @param projectRootDir The root directory of the project. * @returns filePath */ function generatePathFromAnswer(answers:string, projectRootDir:string):string { return path.resolve( projectRootDir, path.dirname(answers) === '.' ? path.resolve(answers) : path.dirname(answers)) } /** * Get project root directory based on questionaire answers * @param answers questionaire answers * @param projectProps project properties received via `getProjectProps` * @returns project root path */ export async function getProjectRoot (parsedAnswers?: Questionnair) { if (parsedAnswers?.createPackageJSON && parsedAnswers.projectRoot) { return parsedAnswers.projectRoot } const root = (await getProjectProps())?.path if (!root) { throw new Error('Could not find project root directory with a package.json') } return !parsedAnswers || parsedAnswers.projectRootCorrect ? root : parsedAnswers.projectRoot || process.cwd() } /** * create package.json if not already existing */ export async function createPackageJSON(parsedAnswers: ParsedAnswers) { const packageJsonExists = await fs.access(path.resolve(process.cwd(), 'package.json')).then(() => true, () => false) // Use the existing package.json if it already exists. if (packageJsonExists) { return } // If a user said no to creating a package.json, but it doesn't exist, abort. if (parsedAnswers.createPackageJSON === false) { /* istanbul ignore if */ if (!packageJsonExists) { console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`) return !process.env.WDIO_UNIT_TESTS && process.exit(0) } return } // Only create if the user gave explicit permission to if (parsedAnswers.createPackageJSON) { console.log(`Creating a ${chalk.bold('package.json')} for the directory...`) await fs.writeFile(path.resolve(process.cwd(), 'package.json'), JSON.stringify({ name: 'webdriverio-tests', version: '0.0.0', private: true, license: 'ISC', type: 'module', dependencies: {}, devDependencies: {} }, null, 2)) console.log(chalk.green(chalk.bold('āœ” Success!\n'))) } } /** * set up TypeScript if it is desired but not installed */ export async function setupTypeScript(parsedAnswers: ParsedAnswers) { /** * don't create a `tsconfig.json` if user doesn't want to use TypeScript */ if (!parsedAnswers.isUsingTypeScript) { return } /** * don't set up TypeScript if a `tsconfig.json` already exists but ensure we install `tsx` * as it is a requirement for running tests with TypeScript */ if (parsedAnswers.hasRootTSConfig) { return } console.log('Setting up TypeScript...') const frameworkPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.framework) const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service)) const serenityTypes = parsedAnswers.serenityAdapter === 'jasmine' ? ['jasmine'] : [] const types = [ 'node', ...(parsedAnswers.framework === 'jasmine' ? ['jasmine'] : []), '@wdio/globals/types', ...(parsedAnswers.framework === 'jasmine' ? ['expect-webdriverio/jasmine'] : ['expect-webdriverio']), ...(parsedAnswers.serenityAdapter ? serenityTypes : [frameworkPackage.package]), ...(parsedAnswers.runner === 'browser' ? ['@wdio/browser-runner'] : []), ...servicePackages .filter(service => ( /** * given that we know that all "official" services have * typescript support we only include them */ service.package.startsWith('@wdio') || /** * also include community maintained packages with known * support for TypeScript */ COMMUNITY_PACKAGES_WITH_TS_SUPPORT.includes(service.package) )).map(service => service.package) ] const preset = getPreset(parsedAnswers) const config = { compilerOptions: { // compiler moduleResolution: 'node', module: !parsedAnswers.esmSupport ? 'commonjs' : 'ESNext', target: 'es2022', lib: ['es2022', 'dom'], types, skipLibCheck: true, // bundler noEmit: true, allowImportingTsExtensions: true, resolveJsonModule: true, isolatedModules: true, // linting strict: true, noUnusedLocals: true, noUnusedParameters: true, noFallthroughCasesInSwitch: true, ...Object.assign( preset === 'lit' ? { experimentalDecorators: true, useDefineForClassFields: false } : {}, preset === 'react' ? { jsx: 'react-jsx' } : {}, preset === 'preact' ? { jsx: 'react-jsx', jsxImportSource: 'preact' } : {}, preset === 'solid' ? { jsx: 'preserve', jsxImportSource: 'solid-js' } : {}, preset === 'stencil' ? { experimentalDecorators: true, jsx: 'react', jsxFactory: 'h', jsxFragmentFactory: 'Fragment' } : {} ) }, include: preset === 'svelte' ? ['src/**/*.d.ts', 'src/**/*.ts', 'src/**/*.js', 'src/**/*.svelte'] : preset === 'vue' ? ['src/**/*.ts', 'src/**/*.d.ts', 'src/**/*.tsx', 'src/**/*.vue'] : ['test', 'wdio.conf.ts'] } if (parsedAnswers.framework === 'cucumber') { config.include.push('features') } await fs.mkdir(path.dirname(parsedAnswers.tsConfigFilePath), { recursive: true }) await fs.writeFile( parsedAnswers.tsConfigFilePath, JSON.stringify(config, null, 4) ) console.log(chalk.green(chalk.bold('āœ” Success!\n'))) } /** * run npm install only if required by the user */ const SEP = '\n- ' export async function npmInstall(parsedAnswers: ParsedAnswers, npmTag: string) { const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service)) const presetPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.preset || '') /** * install Testing Library dependency if desired */ if (parsedAnswers.installTestingLibrary && TESTING_LIBRARY_PACKAGES[presetPackage.short]) { parsedAnswers.packagesToInstall.push( TESTING_LIBRARY_PACKAGES[presetPackage.short], '@testing-library/jest-dom' ) } /** * add helper package for Solidjs testing */ if (presetPackage.short === 'solid') { parsedAnswers.packagesToInstall.push('solid-js') } /** * add visual service if user selected support for it */ if (parsedAnswers.includeVisualTesting) { parsedAnswers.packagesToInstall.push('@wdio/visual-service') } /** * add dependency for Lit testing */ const preset = getPreset(parsedAnswers) if (preset === 'lit') { parsedAnswers.packagesToInstall.push('lit') } /** * add dependency for Stencil testing */ if (preset === 'stencil') { parsedAnswers.packagesToInstall.push('@stencil/core') } /** * add helper for React rendering when not using Testing Library */ if (presetPackage.short === 'react') { parsedAnswers.packagesToInstall.push('react') if (!parsedAnswers.installTestingLibrary) { parsedAnswers.packagesToInstall.push('react-dom') } } /** * add Jasmine types if necessary */ if (parsedAnswers.framework === 'jasmine' && parsedAnswers.isUsingTypeScript) { parsedAnswers.packagesToInstall.push('@types/jasmine') } /** * add Appium mobile drivers if desired */ if (parsedAnswers.purpose === 'macos') { parsedAnswers.packagesToInstall.push('appium-mac2-driver') } if (parsedAnswers.mobileEnvironment === 'android') { parsedAnswers.packagesToInstall.push('appium-uiautomator2-driver') } if (parsedAnswers.mobileEnvironment === 'ios') { parsedAnswers.packagesToInstall.push('appium-xcuitest-driver') } /** * add packages that are required by services */ addServiceDeps(servicePackages, parsedAnswers.packagesToInstall) /** * update package version if CLI is a pre release */ parsedAnswers.packagesToInstall = specifyVersionIfNeeded(parsedAnswers.packagesToInstall, pkg.version, npmTag) const cwd = await getProjectRoot(parsedAnswers) const pm = detectPackageManager() if (parsedAnswers.npmInstall) { console.log(`Installing packages using ${pm}:${SEP}${parsedAnswers.packagesToInstall.join(SEP)}`) const success = await installPackages(cwd, parsedAnswers.packagesToInstall, true) if (success) { console.log(chalk.green(chalk.bold('āœ” Success!\n'))) } } else { const installationCommand = getInstallCommand(pm, parsedAnswers.packagesToInstall, true) console.log(util.format(DEPENDENCIES_INSTALLATION_MESSAGE, installationCommand)) } } function getPreset (parsedAnswers: ParsedAnswers) { const isUsingFramework = typeof parsedAnswers.preset === 'string' return isUsingFramework ? (parsedAnswers.preset || 'lit') : '' } export function addServiceDeps(names: SupportedPackage[], packages: string[], update = false) { /** * install Appium if it is not installed globally if `@wdio/appium-service` * was selected for install */ if (names.some(({ short }) => short === 'appium')) { const result = execSync('appium --version || echo APPIUM_MISSING', { stdio: 'pipe' }).toString().trim() if (result === 'APPIUM_MISSING') { packages.push('appium') } else if (update) { console.log( '\n=======', '\nUsing globally installed appium', result, '\nPlease add the following to your wdio.conf.js:', "\nappium: { command: 'appium' }", '\n=======\n') } } } export async function createWDIOConfig(parsedAnswers: ParsedAnswers) { try { console.log('Creating a WebdriverIO config file...') const tplPath = path.resolve(__dirname, '..', 'templates', 'wdio.conf.tpl.ejs') const renderedTpl = await renderFile(tplPath, { answers: parsedAnswers, _: new EjsHelpers({ useEsm: parsedAnswers.esmSupport, useTypeScript: parsedAnswers.isUsingTypeScript }) }) await fs.writeFile(parsedAnswers.wdioConfigPath, renderedTpl) console.log(chalk.green(chalk.bold('āœ” Success!\n'))) if (parsedAnswers.generateTestFiles) { console.log('Autogenerate test files...') await generateTestFiles(parsedAnswers) console.log(chalk.green(chalk.bold('āœ” Success!\n'))) } } catch (err) { throw new Error(`āš ļø Couldn't write config file: ${(err as Error).stack}`) } } export const renderFile = promisify(ejs.renderFile) as (path: string, data: Record) => Promise /* c8 ignore start */ export async function createWDIOScript(parsedAnswers: ParsedAnswers) { const rootDir = await getProjectRoot(parsedAnswers) const pathToWdioConfig = `./${path.join('.', parsedAnswers.wdioConfigPath.replace(rootDir, ''))}` const wdioScripts = { 'wdio': `wdio run ${ pathToWdioConfig }`, } const serenityScripts = { 'serenity': 'failsafe serenity:clean wdio serenity:report', 'serenity:clean': 'rimraf target', 'wdio': `wdio run ${ pathToWdioConfig }`, 'serenity:report': 'serenity-bdd run', } const scripts = parsedAnswers.serenityAdapter ? serenityScripts : wdioScripts for (const [script, command] of Object.entries(scripts)) { const args = ['pkg', 'set', `scripts.${ script }=${ command }`] try { console.log(`Adding ${chalk.bold(`"${ script }"`)} script to package.json`) await runProgram(NPM_COMMAND, args, { cwd: parsedAnswers.projectRootDir }) } catch (err) { const [preArgs, scriptPath] = args.join(' ').split('=') console.error( `āš ļø Couldn't add script to package.json: "${(err as Error).message}", you can add it manually ` + `by running:\n\n\t${NPM_COMMAND} ${preArgs}="${scriptPath}"` ) return false } } console.log(chalk.green(chalk.bold('āœ” Success!'))) return true } /* c8 ignore stop */ export async function runAppiumInstaller(parsedAnswers: ParsedAnswers) { if (parsedAnswers.e2eEnvironment !== 'mobile') { return } const answer = await inquirer.prompt({ name: 'continueWithAppiumSetup', message: 'Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?', type: 'confirm', default: true }) if (!answer.continueWithAppiumSetup) { return console.log( 'Ok! You can learn more about setting up mobile environments in the ' + 'Appium docs at https://appium.io/docs/en/latest/quickstart/' ) } return $({ stdio: 'inherit' })`npx appium-installer` } export function getSerenityPackages(answers: Questionnair): string[] { const framework = convertPackageHashToObject(answers.framework) if (framework.package !== '@serenity-js/webdriverio') { return [] } const packages: Record> = { cucumber: [ '@cucumber/cucumber', '@serenity-js/cucumber', ], mocha: [ '@serenity-js/mocha', 'mocha', ], jasmine: [ '@serenity-js/jasmine', 'jasmine', ], common: [ '@serenity-js/assertions', '@serenity-js/console-reporter', '@serenity-js/core', '@serenity-js/rest', '@serenity-js/serenity-bdd', '@serenity-js/web', 'npm-failsafe', 'rimraf', ] } if (answers.isUsingTypeScript) { packages.mocha.push('@types/mocha') packages.jasmine.push('@types/jasmine') packages.common.push('@types/node') } return [ ...packages[framework.purpose], ...packages.common, ].filter(Boolean).sort() as string[] } /** * detect the package manager that was used * uses the environment variable `npm_config_user_agent` to detect the package manager * falls back to `npm` if no package manager could be detected */ export function detectPackageManager() { if (!process.env.npm_config_user_agent) { return 'npm' } const detectedPM = process.env.npm_config_user_agent.split('/')[0].toLowerCase() const matchedPM = SUPPORTED_PACKAGE_MANAGERS.find(pm => pm.toLowerCase() === detectedPM) return matchedPM || 'npm' } export async function getDefaultFiles(answers: Questionnair, pattern: string) { const rootDir = await getProjectRoot(answers) const presetPackage = convertPackageHashToObject(answers.preset || '') const isJSX = TSX_BASED_FRAMEWORKS.includes(presetPackage.short || '') const val = pattern.endsWith('.feature') ? path.join(rootDir, pattern) : answers?.isUsingTypeScript ? `${path.join(rootDir, pattern)}.ts${isJSX ? 'x' : ''}` : `${path.join(rootDir, pattern)}.js${isJSX ? 'x' : ''}` return val } const TSX_BASED_FRAMEWORKS = ['react', 'preact', 'solid', 'stencil'] /** * detect if project has a compiler file */ export async function detectCompiler(answers: Questionnair) { // obviously there is no compiler, if there is no package.json if (answers.createPackageJSON) { return false } const root = await getProjectRoot(answers) const hasRootTSConfig = await fs.access(path.resolve(root, 'tsconfig.json')).then(() => true, () => false) return hasRootTSConfig } const VERSION_REGEXP = /(\d+)\.(\d+)\.(\d+)-(alpha|beta|)\.(\d+)\+(.+)/g /** * Ensure core WebdriverIO packages have the same version as cli so that if someone * installs `@wdio/cli@next` and runs the wizard, all related packages have the same version. * running `matchAll` to a version like "8.0.0-alpha.249+4bc237701", results in: * ['8.0.0-alpha.249+4bc237701', '8', '0', '0', 'alpha', '249', '4bc237701'] */ export function specifyVersionIfNeeded(packagesToInstall: string[], version: string, npmTag: string) { const { value } = version.matchAll(VERSION_REGEXP).next() const [major, minor, patch, tagName, build] = (value || []).slice(1, -1) // drop commit bit return packagesToInstall.map((p) => { if ( (p.startsWith('@wdio') && p !== '@wdio/visual-service') || ['webdriver', 'webdriverio'].includes(p) ) { const tag = major && npmTag === 'latest' ? `^${major}.${minor}.${patch}-${tagName}.${build}` : npmTag return `${p}@${tag}` } return p }) } /** * generate test files based on CLI answers */ export async function generateTestFiles(answers: ParsedAnswers) { if (answers.serenityAdapter) { return generateSerenityExamples(answers) } if (answers.runner === 'local') { return generateLocalRunnerTestFiles(answers) } return generateBrowserRunnerTestFiles(answers) } /* c8 ignore start */ async function generateSerenityExamples(answers: ParsedAnswers): Promise { const templateDirectories = Object.entries({ [answers.projectRootDir]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'config'), [answers.destSpecRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', answers.serenityAdapter as string), [answers.destSerenityLibRootPath]: path.join(TEMPLATE_ROOT_DIR, 'serenity-js', 'common', 'serenity'), }) await Promise.all(templateDirectories.map(async ([destinationRootDir, templateRootDir]) => { const pathsToTemplates = await readDir(templateRootDir) await Promise.all(pathsToTemplates.map(async (pathToTemplate) => { const extension = answers.isUsingTypeScript ? '.ts' : '.js' const destination = path.join(destinationRootDir, path.relative(templateRootDir, pathToTemplate)) .replace(/\.ejs$/, '') .replace(/\.ts$/, extension) const contents = await renderFile( pathToTemplate, { answers, _: new EjsHelpers({ useEsm: answers.esmSupport, useTypeScript: answers.isUsingTypeScript }) }, ) await fs.mkdir(path.dirname(destination), { recursive: true }) await fs.writeFile(destination, contents) })) })) } /* c8 ignore stop */ async function generateLocalRunnerTestFiles(answers: ParsedAnswers) { const testFiles = answers.framework === 'cucumber' ? [path.join(TEMPLATE_ROOT_DIR, 'cucumber')] : [path.join(TEMPLATE_ROOT_DIR, 'mochaJasmine')] if (answers.usePageObjects) { testFiles.push(path.join(TEMPLATE_ROOT_DIR, 'pageobjects')) } 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)], []) await Promise.all(files.map(async (file) => { const renderedTpl = await renderFile(file, { answers }) const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset) const fileEnding = (answers.isUsingTypeScript ? '.ts' : '.js') + (isJSX ? 'x' : '') const destPath = ( file.endsWith('page.js.ejs') ? path.join(answers.destPageObjectRootPath, path.basename(file)) : file.includes('step_definition') ? path.join(answers.destStepRootPath, path.basename(file)) : path.join(answers.destSpecRootPath, path.basename(file)) ).replace(/\.ejs$/, '').replace(/\.js$/, fileEnding) await fs.mkdir(path.dirname(destPath), { recursive: true }) await fs.writeFile(destPath, renderedTpl) })) } export async function generateBrowserRunnerTestFiles(answers: ParsedAnswers) { const isUsingFramework = typeof answers.preset === 'string' const preset = getPreset(answers) const tplRootDir = path.join(TEMPLATE_ROOT_DIR, 'browser') await fs.mkdir(answers.destSpecRootPath, { recursive: true }) /** * render css file */ if (isUsingFramework) { const renderedCss = await renderFile(path.join(tplRootDir, 'Component.css.ejs'), { answers }) await fs.writeFile(path.join(answers.destSpecRootPath, 'Component.css'), renderedCss) } /** * render component file */ const testExt = `${(answers.isUsingTypeScript ? 'ts' : 'js')}${TSX_BASED_FRAMEWORKS.includes(preset) ? 'x' : ''}` const fileExt = ['svelte', 'vue'].includes(preset as 'svelte' | 'vue') ? preset! : testExt if (preset) { const componentOutFileName = `Component.${fileExt}` const renderedComponent = await renderFile(path.join(tplRootDir, `Component.${preset}.ejs`), { answers }) await fs.writeFile(path.join(answers.destSpecRootPath, componentOutFileName), renderedComponent) } /** * render test file */ const componentFileName = preset ? `Component.${preset}.test.ejs` : 'standalone.test.ejs' const renderedTest = await renderFile(path.join(tplRootDir, componentFileName), { answers }) await fs.writeFile(path.join(answers.destSpecRootPath, `Component.test.${testExt}`), renderedTest) } /** * Helper utility used in `run` and `install` command to format a provided config path, * giving it back as an absolute path, and a version without the file extension * @param config the initially given file path to the WDIO config file */ export async function formatConfigFilePaths(config: string) { const fullPath = path.isAbsolute(config) ? config : path.join(process.cwd(), config) const fullPathNoExtension = fullPath.substring(0, fullPath.lastIndexOf(path.extname(fullPath))) return { fullPath, fullPathNoExtension } } export function findInConfig(config: string, type: string) { let regexStr = `[\\/\\/]*[\\s]*${type}s: [\\s]*\\[([\\s]*['|"]\\w*['|"],*)*[\\s]*\\]` if (type === 'framework') { regexStr = `[\\/\\/]*[\\s]*${type}: ([\\s]*['|"]\\w*['|"])` } const regex = new RegExp(regexStr, 'gmi') return config.match(regex) } function buildNewConfigArray(str: string, type: string, change: string) { const newStr = str .split(`${type}s: `)[1] .replace(/'/g, '') const newArray = newStr.match(/(\w*)/gmi)?.filter(e => !!e).concat([change]) || [] return str .replace('// ', '') .replace( new RegExp(`(${type}s: )((.*\\s*)*)`), `$1[${newArray.map(e => `'${e}'`)}]` ) } function buildNewConfigString(str: string, type: string, change: string) { return str.replace(new RegExp(`(${type}: )('\\w*')`), `$1'${change}'`) } export function replaceConfig(config: string, type: string, name: string) { if (type === 'framework') { return buildNewConfigString(config, type, name) } const match = findInConfig(config, type) if (!match || match.length === 0) { return } const text = match.pop() || '' return config.replace(text, buildNewConfigArray(text, type, name)) }