UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

291 lines (255 loc) 7.93 kB
const { existsSync, readFileSync } = require('fs') const { globSync } = require('glob') const fsPath = require('path') const { resolve } = require('path') const container = require('./container') const Config = require('./config') const event = require('./event') const runHook = require('./hooks') const output = require('./output') const { emptyFolder } = require('./utils') /** * CodeceptJS runner */ class Codecept { /** * Create CodeceptJS runner. * Config and options should be passed * * @param {*} config * @param {*} opts */ constructor(config, opts) { this.config = Config.create(config) this.opts = opts this.testFiles = new Array(0) this.requireModules(config.require) } /** * Require modules before codeceptjs running * * @param {string[]} requiringModules */ requireModules(requiringModules) { if (requiringModules) { requiringModules.forEach(requiredModule => { const isLocalFile = existsSync(requiredModule) || existsSync(`${requiredModule}.js`) if (isLocalFile) { requiredModule = resolve(requiredModule) } require(requiredModule) }) } } /** * Initialize CodeceptJS at specific directory. * If async initialization is required, pass callback as second parameter. * * @param {string} dir */ init(dir) { this.initGlobals(dir) // initializing listeners container.create(this.config, this.opts) this.runHooks() } /** * Creates global variables * * @param {string} dir */ initGlobals(dir) { global.codecept_dir = dir global.output_dir = fsPath.resolve(dir, this.config.output) if (this.config.emptyOutputFolder) emptyFolder(global.output_dir) if (!this.config.noGlobals) { global.Helper = global.codecept_helper = require('@codeceptjs/helper') global.actor = global.codecept_actor = require('./actor') global.pause = require('./pause') global.within = require('./within') global.session = require('./session') global.DataTable = require('./data/table') global.locate = locator => require('./locator').build(locator) global.inject = container.support global.share = container.share global.secret = require('./secret').secret global.codecept_debug = output.debug global.codeceptjs = require('./index') // load all objects // BDD const stepDefinitions = require('./mocha/bdd') global.Given = stepDefinitions.Given global.When = stepDefinitions.When global.Then = stepDefinitions.Then global.DefineParameterType = stepDefinitions.defineParameterType // debug mode global.debugMode = false // mask sensitive data global.maskSensitiveData = this.config.maskSensitiveData || false } } /** * Executes hooks. */ runHooks() { // default hooks runHook(require('./listener/store')) runHook(require('./listener/steps')) runHook(require('./listener/config')) runHook(require('./listener/result')) runHook(require('./listener/helpers')) runHook(require('./listener/globalTimeout')) runHook(require('./listener/globalRetry')) runHook(require('./listener/retryEnhancer')) runHook(require('./listener/exit')) runHook(require('./listener/emptyRun')) // custom hooks (previous iteration of plugins) this.config.hooks.forEach(hook => runHook(hook)) } /** * Executes bootstrap. * * @returns {Promise<void>} */ async bootstrap() { return runHook(this.config.bootstrap, 'bootstrap') } /** * Executes teardown. * * @returns {Promise<void>} */ async teardown() { return runHook(this.config.teardown, 'teardown') } /** * Loads tests by pattern or by config.tests * * @param {string} [pattern] */ loadTests(pattern) { const options = { cwd: global.codecept_dir, } let patterns = [pattern] if (!pattern) { patterns = [] // If the user wants to test a specific set of test files as an array or string. if (this.config.tests && !this.opts.features) { if (Array.isArray(this.config.tests)) { patterns.push(...this.config.tests) } else { patterns.push(this.config.tests) } } if (this.config.gherkin.features && !this.opts.tests) { if (Array.isArray(this.config.gherkin.features)) { this.config.gherkin.features.forEach(feature => { patterns.push(feature) }) } else { patterns.push(this.config.gherkin.features) } } } for (pattern of patterns) { if (pattern) { globSync(pattern, options).forEach(file => { if (file.includes('node_modules')) return if (!fsPath.isAbsolute(file)) { file = fsPath.join(global.codecept_dir, file) } if (!this.testFiles.includes(fsPath.resolve(file))) { this.testFiles.push(fsPath.resolve(file)) } }) } } this.testFiles.sort() if (this.opts.shuffle) { this.testFiles = this.shuffle(this.testFiles) } if (this.opts.shard) { this.testFiles = this._applySharding(this.testFiles, this.opts.shard) } } /** * Fisher–Yates shuffle (non-mutating) */ shuffle(array) { const arr = [...array] // clone to avoid mutating input for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[arr[i], arr[j]] = [arr[j], arr[i]] // swap } return arr } /** * Apply sharding to test files based on shard configuration * * @param {Array<string>} testFiles - Array of test file paths * @param {string} shardConfig - Shard configuration in format "index/total" (e.g., "1/4") * @returns {Array<string>} - Filtered array of test files for this shard */ _applySharding(testFiles, shardConfig) { const shardMatch = shardConfig.match(/^(\d+)\/(\d+)$/) if (!shardMatch) { throw new Error('Invalid shard format. Expected format: "index/total" (e.g., "1/4")') } const shardIndex = parseInt(shardMatch[1], 10) const shardTotal = parseInt(shardMatch[2], 10) if (shardTotal < 1) { throw new Error('Shard total must be at least 1') } if (shardIndex < 1 || shardIndex > shardTotal) { throw new Error(`Shard index ${shardIndex} must be between 1 and ${shardTotal}`) } if (testFiles.length === 0) { return testFiles } // Calculate which tests belong to this shard const shardSize = Math.ceil(testFiles.length / shardTotal) const startIndex = (shardIndex - 1) * shardSize const endIndex = Math.min(startIndex + shardSize, testFiles.length) return testFiles.slice(startIndex, endIndex) } /** * Run a specific test or all loaded tests. * * @param {string} [test] * @returns {Promise<void>} */ async run(test) { await container.started() return new Promise((resolve, reject) => { const mocha = container.mocha() mocha.files = this.testFiles if (test) { if (!fsPath.isAbsolute(test)) { test = fsPath.join(global.codecept_dir, test) } mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test) } const done = () => { event.emit(event.all.result, container.result()) event.emit(event.all.after, this) resolve() } try { event.emit(event.all.before, this) mocha.run(() => done()) } catch (e) { output.error(e.stack) reject(e) } }) } /** * Returns the version string of CodeceptJS. * * @returns {string} The version string. */ static version() { return JSON.parse(readFileSync(`${__dirname}/../package.json`, 'utf8')).version } } module.exports = Codecept