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<void>((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 {
}
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)
}
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<Questionnair> {
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,
when: (answers: Questionnair) => isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault && !answers.projectRootCorrect
}
]
const answers = await inquirer.prompt(projectRootQuestions)
if (answers.createPackageJSON) {
answers.projectRoot = process.cwd()
}
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<ProjectProps | undefined> {
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)
if (packageJsonExists) {
return
}
if (parsedAnswers.createPackageJSON === false) {
if (!packageJsonExists) {
console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`)
return !process.env.WDIO_UNIT_TESTS && process.exit(0)
}
return
}
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: {
moduleResolution: 'node',
module: !parsedAnswers.esmSupport ? 'commonjs' : 'ESNext',
target: 'es2022',
lib: ['es2022', 'dom'],
types,
skipLibCheck: true,
noEmit: true,
allowImportingTsExtensions: true,
resolveJsonModule: true,
isolatedModules: true,
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<string, unknown>) => Promise<string>
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
}
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<string, Array<string | false>> = {
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) {
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)
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)
}
async function generateSerenityExamples(answers: ParsedAnswers): Promise<void> {
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)
}))
}))
}
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))
}