@kronoslive/codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
471 lines (428 loc) • 13.1 kB
JavaScript
const glob = require('glob');
const path = require('path');
const { MetaStep } = require('./step');
const { fileExists, isFunction, isAsyncFunction } = require('./utils');
const Translation = require('./translation');
const MochaFactory = require('./mochaFactory');
const recorder = require('./recorder');
const event = require('./event');
const Step = require('./step');
const WorkerStorage = require('./workerStorage');
let container = {
helpers: {},
support: {},
plugins: {},
/**
* @type {Mocha | {}}
* @ignore
*/
mocha: {},
translation: {},
};
/**
* Dependency Injection Container
*/
class Container {
/**
* Create container with all required helpers and support objects
*
* @api
* @param {*} config
* @param {*} opts
*/
static create(config, opts) {
const mochaConfig = config.mocha || {};
if (config.grep && !opts.grep) {
mochaConfig.grep = config.grep;
}
this.createMocha = () => {
container.mocha = MochaFactory.create(mochaConfig, opts || {});
};
this.createMocha();
container.helpers = createHelpers(config.helpers || {});
container.translation = loadTranslation(config.translation || null);
container.support = createSupportObjects(config.include || {});
container.plugins = createPlugins(config.plugins || {}, opts);
if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
}
/**
* Get all plugins
*
* @api
* @param {string} [name]
* @returns { * }
*/
static plugins(name) {
if (!name) {
return container.plugins;
}
return container.plugins[name];
}
/**
* Get all support objects or get support object by name
*
* @api
* @param {string} [name]
* @returns { * }
*/
static support(name) {
if (!name) {
return container.support;
}
return container.support[name];
}
/**
* Get all helpers or get a helper by name
*
* @api
* @param {string} [name]
* @returns { * }
*/
static helpers(name) {
if (!name) {
return container.helpers;
}
return container.helpers[name];
}
/**
* Get translation
*
* @api
*/
static translation() {
return container.translation;
}
/**
* Get Mocha instance
*
* @api
* @returns { * }
*/
static mocha() {
return container.mocha;
}
/**
* Append new services to container
*
* @api
* @param {Object<string, *>} newContainer
*/
static append(newContainer) {
const deepMerge = require('./utils').deepMerge;
container = deepMerge(container, newContainer);
}
/**
* Clear container
*
* @param {Object<string, *>} newHelpers
* @param {Object<string, *>} newSupport
* @param {Object<string, *>} newPlugins
*/
static clear(newHelpers, newSupport, newPlugins) {
container.helpers = newHelpers || {};
container.support = newSupport || {};
container.plugins = newPlugins || {};
container.translation = loadTranslation();
}
/**
* Share data across worker threads
*
* @param {Object} data
* @param {Object} options - set {local: true} to not share among workers
*/
static share(data, options = {}) {
Container.append({ support: data });
if (!options.local) {
WorkerStorage.share(data);
}
}
}
module.exports = Container;
function createHelpers(config) {
const helpers = {};
let moduleName;
for (const helperName in config) {
try {
if (config[helperName].require) {
if (config[helperName].require.startsWith('.')) {
moduleName = path.resolve(global.codecept_dir, config[helperName].require); // custom helper
} else {
moduleName = config[helperName].require; // plugin helper
}
} else {
moduleName = `./helper/${helperName}`; // built-in helper
}
let HelperClass = require(moduleName);
if (HelperClass.default) {
HelperClass = HelperClass.default;
}
if (HelperClass._checkRequirements) {
const requirements = HelperClass._checkRequirements();
if (requirements) {
let install;
if (require('./utils').installedLocally()) {
install = `npm install --save-dev ${requirements.join(' ')}`;
} else {
install = `[sudo] npm install -g ${requirements.join(' ')}`;
}
throw new Error(`Required modules are not installed.\n\nRUN: ${install}`);
}
}
helpers[helperName] = new HelperClass(config[helperName]);
} catch (err) {
throw new Error(`Could not load helper ${helperName} from module '${moduleName}':\n${err.message}\n${err.stack}`);
}
}
for (const name in helpers) {
if (helpers[name]._init) helpers[name]._init();
}
return helpers;
}
function createSupportObjects(config) {
const objects = {};
for (const name in config) {
objects[name] = {}; // placeholders
}
if (!config.I) {
objects.I = require('./actor')();
if (container.translation.I !== 'I') {
objects[container.translation.I] = objects.I;
}
}
container.support = objects;
function lazyLoad(name) {
let newObj = getSupportObject(config, name);
try {
if (typeof newObj === 'function') {
newObj = newObj();
} else if (newObj._init) {
newObj._init();
}
} catch (err) {
throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}`);
}
return newObj;
}
const asyncWrapper = function (o, m, f) {
return function () {
const step = new Step();
step.setArguments(Object.values(arguments));
step.name = m; // method name
step.actor = o; // / actor name
// f - function
step.setCallTree();
return f.apply(this, arguments).catch((e) => {
recorder.saveFirstAsyncError(e);
throw e;
});
};
};
const functionWrapper = function (o, m, f) {
return function () {
const step = new Step();
step.setArguments(Object.values(arguments));
step.name = m;
step.actor = o;
step.setCallTree();
return f.apply(this, arguments);
};
};
const wrapper = (object, actorName, methodName) => new Proxy(object, {
get(target, propKey) {
if (target.default) {
target = target.default;
}
if (typeof propKey === 'symbol') {
return target[propKey];
}
const newMethodName = `${methodName ? `${methodName} ` : ''}${propKey}`;
if (Object.prototype.toString.call(target[propKey]) === '[object Object]') {
// ignore chai
if (target[propKey].constructor && target[propKey].constructor.name === 'Assertion') {
return target[propKey];
}
return wrapper(target[propKey], actorName, newMethodName);
}
if (typeof target[propKey] === 'function' && propKey !== 'toString') {
if (target[propKey][Symbol.toStringTag] === 'AsyncFunction') {
return asyncWrapper(actorName, newMethodName, target[propKey]);
}
return functionWrapper(actorName, newMethodName, target[propKey]);
}
return target[propKey];
},
});
Object.keys(objects).forEach((objectName) => {
objects[objectName] = wrapper(objects[objectName], objectName);
});
return new Proxy({}, {
has(target, key) {
return key in config;
},
ownKeys() {
return Reflect.ownKeys(config);
},
get(target, key) {
// configured but not in support object, yet: load the module
if (key in objects && !(key in target)) {
// load default I
if (key in objects && !(key in config)) {
return target[key] = objects[key];
}
// load new object
const object = lazyLoad(key);
// check that object is a real object and not an array
if (Object.prototype.toString.call(object) === '[object Object]') {
return target[key] = Object.assign(objects[key], object);
}
target[key] = object;
}
return target[key];
},
});
}
function createPlugins(config, options = {}) {
const plugins = {};
const enabledPluginsByOptions = (options.plugins || '').split(',');
for (const pluginName in config) {
if (!config[pluginName]) config[pluginName] = {};
if (!config[pluginName].enabled && (enabledPluginsByOptions.indexOf(pluginName) < 0)) {
continue; // plugin is disabled
}
let module;
try {
if (config[pluginName].require) {
module = config[pluginName].require;
if (module.startsWith('.')) { // local
module = path.resolve(global.codecept_dir, module); // custom plugin
}
} else {
module = `./plugin/${pluginName}`;
}
plugins[pluginName] = require(module)(config[pluginName]);
} catch (err) {
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}`);
}
}
return plugins;
}
function getSupportObject(config, name) {
const module = config[name];
if (typeof module === 'string') {
return loadSupportObject(module, name);
}
return module;
}
function loadGherkinSteps(paths) {
global.Before = fn => event.dispatcher.on(event.test.started, fn);
global.After = fn => event.dispatcher.on(event.test.finished, fn);
global.Fail = fn => event.dispatcher.on(event.test.failed, fn);
// If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
// If gherkin.steps is Array, it will go the old way
// This is done so that we need not enter all Step Definition files under config.gherkin.steps
if (Array.isArray(paths)) {
for (const path of paths) {
loadSupportObject(path, `Step Definition from ${path}`);
}
} else {
const folderPath = paths.startsWith('.') ? path.join(global.codecept_dir, paths) : '';
if (folderPath !== '') {
glob.sync(folderPath).forEach((file) => {
loadSupportObject(file, `Step Definition from ${file}`);
});
}
}
delete global.Before;
delete global.After;
delete global.Fail;
}
function loadSupportObject(modulePath, supportObjectName) {
if (modulePath.charAt(0) === '.') {
modulePath = path.join(global.codecept_dir, modulePath);
}
try {
const obj = require(modulePath);
if (typeof obj === 'function') {
const fobj = obj();
if (fobj.constructor.name === 'Actor') {
const methods = getObjectMethods(fobj);
Object.keys(methods)
.forEach(key => {
fobj[key] = methods[key];
});
return methods;
}
}
if (typeof obj !== 'function'
&& Object.getPrototypeOf(obj) !== Object.prototype
&& !Array.isArray(obj)
) {
const methods = getObjectMethods(obj);
Object.keys(methods)
.filter(key => !key.startsWith('_'))
.forEach(key => {
const currentMethod = methods[key];
if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
const ms = new MetaStep(supportObjectName, key);
ms.setContext(methods);
methods[key] = ms.run.bind(ms, currentMethod);
}
});
return methods;
}
if (!Array.isArray(obj)) {
Object.keys(obj)
.filter(key => !key.startsWith('_'))
.forEach(key => {
const currentMethod = obj[key];
if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
const ms = new MetaStep(supportObjectName, key);
ms.setContext(obj);
obj[key] = ms.run.bind(ms, currentMethod);
}
});
}
return obj;
} catch (err) {
throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}`);
}
}
/**
* Method collect own property and prototype
*/
function getObjectMethods(obj) {
const methodsSet = new Set();
let protoObj = Reflect.getPrototypeOf(obj);
do {
if (protoObj.constructor.prototype !== Object.prototype) {
const keys = Reflect.ownKeys(protoObj);
keys.forEach(k => methodsSet.add(k));
}
} while (protoObj = Reflect.getPrototypeOf(protoObj));
Reflect.ownKeys(obj).forEach(k => methodsSet.add(k));
const methods = {};
for (const key of methodsSet.keys()) {
if (key !== 'constructor') methods[key] = obj[key];
}
return methods;
}
function loadTranslation(translation) {
if (!translation) {
return new Translation({
I: 'I',
actions: {},
}, false);
}
let vocabulary;
// check if it is a known translation
if (require('../translations')[translation]) {
vocabulary = require('../translations')[translation];
return new Translation(vocabulary);
} if (fileExists(path.join(global.codecept_dir, translation))) {
// get from a provided file instead
vocabulary = require(path.join(global.codecept_dir, translation));
} else {
throw new Error(`Translation option is set in config, but ${translation} is not a translated locale or filename`);
}
return new Translation(vocabulary);
}