codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
410 lines (343 loc) • 12.2 kB
JavaScript
import colors from 'chalk'
import fs from 'fs'
import inquirer from 'inquirer'
import { mkdirp } from 'mkdirp'
import path from 'path'
import { inspect } from 'util'
import spawn from 'cross-spawn'
import output from '../output.js'
const { print, success, error } = output
import { fileExists, beautify, installedLocally } from '../utils.js'
import { getTestRoot } from './utils.js'
import generateDefinitions from './definitions.js'
import { test as generateTest } from './generate.js'
const isLocal = installedLocally()
const defaultConfig = {
tests: './tests/*_test.js',
output: '',
helpers: {},
include: {},
noGlobals: true,
plugins: {
},
}
const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium']
const packages = []
let isTypeScript = false
let extension = 'js'
const importCodeceptConfigure = "import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';"
const configHeader = `
// turn on headless mode when running with HEADLESS=true environment variable
// export HEADLESS=true && npx codeceptjs run
setHeadlessWhen(process.env.HEADLESS);
// enable all common plugins https://github.com/codeceptjs/configure#setcommonplugins
setCommonPlugins();
`
const defaultActor = `// in this file you can append custom step methods to 'I' object
import { actor } from 'codeceptjs';
export default function() {
return actor({
// Define custom steps here, use 'this' to access default methods of I.
// It is recommended to place a general 'login' function here.
});
}
`
const defaultActorTs = `// in this file you can append custom step methods to 'I' object
import { actor } from 'codeceptjs';
export default function() {
return actor({
// Define custom steps here, use 'this' to access default methods of I.
// It is recommended to place a general 'login' function here.
});
}
`
export default async function (initPath, options = {}) {
const testsPath = getTestRoot(initPath)
const skipPrompts = !!options.yes
print()
print(` Welcome to ${colors.magenta.bold('CodeceptJS')} initialization tool`)
print(' It will prepare and configure a test environment for you')
print()
print(' Useful links:')
print()
print(' 👉 How to start testing ASAP: https://codecept.io/quickstart/#init')
print(' 👉 How to select helper: https://codecept.io/basics/#architecture')
print(' 👉 TypeScript setup: https://codecept.io/typescript/#getting-started')
print()
if (!path) {
print('No test root specified.')
print(`Test root is assumed to be ${colors.yellow.bold(testsPath)}`)
print('----------------------------------')
} else {
print(`Installing to ${colors.bold(testsPath)}`)
}
if (!fileExists(testsPath)) {
print(`Directory ${testsPath} does not exist, creating...`)
mkdirp.sync(testsPath)
}
const configFile = path.join(testsPath, 'codecept.conf.js')
if (fileExists(configFile)) {
error(`Config is already created at ${configFile}`)
return
}
const typeScriptconfigFile = path.join(testsPath, 'codecept.conf.ts')
if (fileExists(typeScriptconfigFile)) {
error(`Config is already created at ${typeScriptconfigFile}`)
return
}
const result = skipPrompts
? {
typescript: false,
tests: './tests/*_test.js',
helper: 'Playwright',
jsonResponse: false,
output: './output',
}
: await inquirer.prompt([
{
name: 'typescript',
type: 'confirm',
default: false,
message: 'Do you plan to write tests in TypeScript?',
},
{
name: 'tests',
type: 'input',
default: answers => `./tests/*_test.${answers.typescript ? 'ts' : 'js'}`,
message: 'Where are your tests located?',
},
{
name: 'helper',
type: 'list',
choices: helpers,
default: 'Playwright',
message: 'What helpers do you want to use?',
},
{
name: 'jsonResponse',
type: 'confirm',
default: true,
message: 'Do you want to use JSONResponse helper for assertions on JSON responses? http://bit.ly/3ASVPy9',
when: answers => ['GraphQL', 'REST'].includes(answers.helper) === true,
},
{
name: 'output',
default: './output',
message: 'Where should logs, screenshots, and reports to be stored?',
},
])
if (result.typescript === true) {
isTypeScript = true
extension = isTypeScript === true ? 'ts' : 'js'
packages.push('typescript')
packages.push('tsx')
packages.push('@types/node')
}
const config = defaultConfig
config.name = testsPath.split(path.sep).pop()
config.output = result.output
config.tests = result.tests
if (isTypeScript) {
config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
config.require = ['tsx/cjs']
}
const matchResults = config.tests.match(/[^*.]+/)
if (matchResults) {
mkdirp.sync(path.join(testsPath, matchResults[0]))
}
const helperName = result.helper
config.helpers[helperName] = {}
if (result.jsonResponse === true) {
config.helpers.JSONResponse = {}
}
let helperConfigs = []
try {
const HelperModule = await import(`../helper/${helperName}.js`)
const Helper = HelperModule.default || HelperModule
if (Helper._checkRequirements) {
packages.concat(Helper._checkRequirements())
}
if (!Helper._config()) return
helperConfigs = helperConfigs.concat(
Helper._config().map(config => {
config.message = `[${helperName}] ${config.message}`
config.name = `${helperName}_${config.name}`
config.type = config.type || 'input'
return config
}),
)
} catch (err) {
error(err)
}
const finish = async () => {
const stepFile = `./steps_file.${extension}`
fs.writeFileSync(path.join(testsPath, stepFile), extension === 'ts' ? defaultActorTs : defaultActor)
config.include = { I: isTypeScript ? './steps_file.ts' : stepFile }
print(`Steps file created at ${stepFile}`)
let configSource
const hasConfigure = isLocal && !initPath
if (isTypeScript) {
configSource = beautify(`export const config : CodeceptJS.MainConfig = ${inspect(config, false, 4, false)}`)
if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource
fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8')
print(`Config created at ${typeScriptconfigFile}`)
} else {
configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexport const config = ${inspect(config, false, 4, false)}`)
if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource
fs.writeFileSync(configFile, configSource, 'utf-8')
print(`Config created at ${configFile}`)
}
if (config.output) {
if (!fileExists(config.output)) {
mkdirp.sync(path.join(testsPath, config.output))
print(`Directory for temporary output files created at '${config.output}'`)
} else {
print(`Directory for temporary output files is already created at '${config.output}'`)
}
}
const jsconfig = {
compilerOptions: {
allowJs: true,
},
}
const tsconfig = {
compilerOptions: {
target: 'ES2022',
lib: ['ES2022', 'DOM'],
esModuleInterop: true,
module: 'ESNext',
moduleResolution: 'bundler',
strictNullChecks: false,
types: ['codeceptjs', 'node'],
declaration: true,
skipLibCheck: true,
},
exclude: ['node_modules'],
}
if (isTypeScript) {
const tsconfigJson = beautify(JSON.stringify(tsconfig))
const tsconfigFile = path.join(testsPath, 'tsconfig.json')
if (fileExists(tsconfigFile)) {
print(`tsconfig.json already exists at ${tsconfigFile}`)
} else {
fs.writeFileSync(tsconfigFile, tsconfigJson)
}
} else {
const jsconfigJson = beautify(JSON.stringify(jsconfig))
const jsconfigFile = path.join(testsPath, 'jsconfig.json')
if (fileExists(jsconfigFile)) {
print(`jsconfig.json already exists at ${jsconfigFile}`)
} else {
fs.writeFileSync(jsconfigFile, jsconfigJson)
print(`Intellisense enabled in ${jsconfigFile}`)
}
}
const generateDefinitionsManually = colors.bold(`To get auto-completion support, please generate type definitions: ${colors.green('npx codeceptjs def')}`)
if (packages) {
try {
install(packages)
} catch (err) {
print(colors.bold.red(err.toString()))
print()
print(colors.bold.red('Please install next packages manually:'))
print(`npm i ${packages.join(' ')} --save-dev`)
print()
print('Things to do after missing packages installed:')
print('☑', generateDefinitionsManually)
print('☑ Create first test:', colors.green('npx codeceptjs gt'))
print(colors.bold.magenta('Find more information at https://codecept.io'))
return
}
}
try {
generateDefinitions(testsPath, {})
} catch (err) {
print(colors.bold.red("Couldn't generate type definitions"))
print(colors.red(err.toString()))
print('Skipping type definitions...')
print(generateDefinitionsManually)
}
print('')
success(' Almost ready... Next step:')
if (skipPrompts) {
print('\n--')
print(colors.bold.green('CodeceptJS Installed! Enjoy supercharged testing! 🤩'))
print(colors.bold.magenta('Find more information at https://codecept.io'))
print()
return
}
if (process.env.CODECEPT_TEST) return
const generatedTest = generateTest(testsPath)
if (!generatedTest) return
generatedTest.then(() => {
print('\n--')
print(colors.bold.green('CodeceptJS Installed! Enjoy supercharged testing! 🤩'))
print(colors.bold.magenta('Find more information at https://codecept.io'))
print()
})
}
const helperResult = skipPrompts
? defaultHelperAnswers(helperName)
: await (async () => {
print('Configure helpers...')
return inquirer.prompt(helperConfigs)
})()
if (helperResult.Playwright_browser === 'electron') {
delete helperResult.Playwright_url
delete helperResult.Playwright_show
helperResult.Playwright_electron = {
executablePath: '// require("electron") or require("electron-forge")',
args: ['path/to/your/main.js'],
}
}
Object.keys(helperResult).forEach(key => {
const parts = key.split('_')
const hName = parts[0]
const configName = parts[1]
if (!configName) return
config.helpers[hName][configName] = helperResult[key]
})
print('')
await finish()
}
function defaultHelperAnswers(helperName) {
if (helperName === 'Playwright') {
return {
Playwright_browser: 'chromium',
Playwright_url: process.env.BASE_URL || 'http://localhost',
Playwright_show: false,
}
}
return {}
}
function install(dependencies) {
let command
let args
if (!fs.existsSync(path.join(process.cwd(), 'package.json'))) {
dependencies.push('codeceptjs')
throw new Error("Error: 'package.json' file not found. Generate it with 'npm init -y' command.")
}
if (!installedLocally()) {
console.log('CodeceptJS should be installed locally')
dependencies.push('codeceptjs')
}
console.log('Installing packages: ', colors.green(dependencies.join(', ')))
if (fileExists('yarn.lock')) {
command = 'yarnpkg'
args = ['add', '-D', '--exact']
;[].push.apply(args, dependencies)
args.push('--cwd')
args.push(process.cwd())
} else {
command = 'npm'
args = ['install', '--save-dev', '--loglevel', 'error'].concat(dependencies)
}
if (process.env._INIT_DRY_RUN_INSTALL) {
args.push('--dry-run')
}
const { status } = spawn.sync(command, args, { stdio: 'inherit' })
if (status !== 0) {
throw new Error(`${command} ${args.join(' ')} failed`)
}
return true
}