UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

410 lines (343 loc) 12.2 kB
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 }