codeceptjs
Version:
Modern Era Acceptance Testing Framework for NodeJS
240 lines (211 loc) • 5.76 kB
JavaScript
const store = require('./store');
const Secret = require('./secret');
const STACK_LINE = 4;
// using support objetcs for metastep detection
// deprecated
let support;
/**
* Each command in test executed through `I.` object is wrapped in Step.
* Step allows logging executed commands and triggers hook before and after step execution.
* @param {CodeceptJS.Helper} helper
* @param {string} name
*/
class Step {
constructor(helper, name) {
/** @member {string} */
this.actor = 'I'; // I = actor
/** @member {CodeceptJS.Helper} */
this.helper = helper; // corresponding helper
/** @member {string} */
this.name = name; // name of a step console
/** @member {string} */
this.helperMethod = name; // helper method
/** @member {string} */
this.status = 'pending';
/**
* @member {string} suffix
* @memberof CodeceptJS.Step#
*/
/** @member {string} */
this.prefix = this.suffix = '';
/** @member {string} */
this.comment = '';
/** @member {Array<*>} */
this.args = [];
/** @member {string} */
this.stack = '';
this.setTrace();
}
/** @function */
setTrace() {
Error.captureStackTrace(this);
/** @member {MetaStep} */
this.metaStep = detectMetaStep(this.stack.split('\n'));
}
/** @param {Array<*>} args */
setArguments(args) {
this.args = args;
}
/**
* @param {...any} args
* @return {*}
*/
run() {
this.args = Array.prototype.slice.call(arguments);
if (store.dryRun) {
this.setStatus('success');
return Promise.resolve(new Proxy({}, dryRunResolver()));
}
let result;
try {
result = this.helper[this.helperMethod].apply(this.helper, this.args);
this.setStatus('success');
} catch (err) {
this.setStatus('failed');
throw err;
}
return result;
}
/** @param {string} status */
setStatus(status) {
this.status = status;
if (this.metaStep) this.metaStep.setStatus(status);
}
/** @return {string} */
humanize() {
return this.name
// insert a space before all caps
.replace(/([A-Z])/g, ' $1')
// _ chars to spaces
.replace('_', ' ')
// uppercase the first character
.replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase());
}
/** @return {string} */
humanizeArgs() {
return this.args.map((arg) => {
if (typeof arg === 'string') {
return `"${arg}"`;
} else if (Array.isArray(arg)) {
try {
const res = JSON.stringify(arg);
return res;
} catch (err) {
return `[${arg.toString()}]`;
}
} else if (typeof arg === 'function') {
return arg.toString();
} else if (typeof arg === 'undefined') {
return `${arg}`;
} else if (arg instanceof Secret) {
return '*****';
} else if (arg.toString && arg.toString() !== '[object Object]') {
return arg.toString();
} else if (typeof arg === 'object') {
return JSON.stringify(arg);
}
return arg;
}).join(', ');
}
/** @return {string} */
line() {
const lines = this.stack.split('\n');
if (lines[STACK_LINE]) return lines[STACK_LINE].trim();
return '';
}
/** @return {string} */
toString() {
return `${this.prefix}${this.actor} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`;
}
/** @return {string} */
toCode() {
return `${this.prefix}${this.actor}.${this.name}(${this.humanizeArgs()})${this.suffix}`;
}
isMetaStep() {
return this.constructor.name === 'MetaStep';
}
/** @return {boolean} */
hasBDDAncestor() {
let hasBDD = false;
let processingStep;
processingStep = this;
while (processingStep.metaStep) {
if (processingStep.metaStep.actor.match(/^(Given|When|Then|And)/)) {
hasBDD = true;
break;
} else {
processingStep = processingStep.metaStep;
}
}
return hasBDD;
}
}
/** @extends Step */
class MetaStep extends Step {
constructor(obj, method) {
super(null, method);
this.actor = obj;
}
humanize() {
return this.name;
}
setTrace() {
}
/** @return {void} */
run() {
}
}
/** @type {Class<MetaStep>} */
Step.MetaStep = MetaStep;
module.exports = Step;
function detectMetaStep(stack) {
if (store.debugMode) return; // no detection in debug
if (!support) loadSupportObjects(); // deprecated
for (let i = STACK_LINE; i < stack.length; i++) {
const line = stack[i].trim();
if (isTest(line) || isBDD(line)) break;
const fnName = line.match(/^at (\w+)\.(\w+)\s\(/);
if (!fnName) continue;
if (fnName[1] === 'Generator'
|| fnName[1] === 'recorder'
|| fnName[1] === 'Runner'
) { return; } // don't track meta steps inside generators
if (fnName[1] === 'Object') {
// detect PO name from includes
for (const name in support) {
const file = support[name];
if (line.indexOf(file) > -1) {
return new MetaStep(`${name}:`, fnName[2]);
}
}
}
return new MetaStep(`${fnName[1]}:`, fnName[2]);
}
}
function loadSupportObjects() {
const config = require('./config');
const path = require('path');
support = {
...config.get('include', {}),
};
if (support) {
for (const name in support) {
const file = support[name];
support[name] = path.join(global.codecept_dir, file);
}
}
}
function isTest(line) {
return line.trim().match(/^at Test\.Scenario/);
}
function isBDD(line) {
return line.trim().match(/^at (Given|When|Then)/);
}
function dryRunResolver() {
return {
get(target, prop) {
if (prop === 'toString') return () => '<VALUE>';
return new Proxy({}, dryRunResolver());
},
};
}