UNPKG

@kronoslive/codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

288 lines (256 loc) 7.52 kB
// TODO: place MetaStep in other file, disable rule /* eslint-disable max-classes-per-file */ const stacktrace = require('stacktrace-js'); const store = require('./store'); const Secret = require('./secret'); const event = require('./event'); const STACK_LINE = 4; const { insertHistory, getLastPageObject } = require('./cliHistory'); const { stringHash } = require('./utils'); /** * 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 = this.sessionPrefix = ''; /** @member {string} */ this.comment = ''; this.callTree = []; /** @member {Array<*>} */ this.args = []; /** @member {MetaStep} */ this.metaStep = undefined; /** @member {string} */ this.stack = ''; this.setTrace(); } /** @function */ setTrace() { Error.captureStackTrace(this); } setCallTree() { const dd = stacktrace.getSync().reverse(); dd.forEach((trace) => { if (trace.functionName) { if (trace.functionName.indexOf('Scenario') > -1 || trace.functionName.indexOf('beforeSuite') > -1 || trace.functionName.indexOf('before') > -1 || trace.functionName.indexOf('afterSuite') > -1 || trace.functionName.indexOf('after') > -1 || trace.functionName.indexOf('within') > -1 || trace.functionName.indexOf('session') > -1) { this.callTree.push({ id: `${trace.columnNumber}-${trace.lineNumber}-${stringHash(trace.fileName)}`, parentid: 0, }); } if ((trace.functionName.indexOf('Proxy') > -1 || trace.functionName.indexOf('Object') > -1) && trace.functionName.indexOf('Object.keys.map.forEach') === -1 && trace.functionName.indexOf('Object.obj.') === -1 && trace.fileName && trace.fileName.indexOf('container.js') === -1) { if (this.callTree.length === 0) { this.callTree = getLastPageObject(); } this.callTree.push({ id: `${trace.columnNumber}-${trace.lineNumber}-${stringHash(trace.fileName)}`, parentid: this.callTree[this.callTree.length - 1].id, }); } } }); const callTreeElem = this.callTree.length === 0 ? 0 : this.callTree.length - 1; this.callTree[callTreeElem] = { ...this.callTree[callTreeElem], step: { args: this.humanizeArgs(), name: this.humanize(), actor: this.actor }, }; insertHistory(this.callTree); } /** @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 (arg === null) { return 'null'; } if (typeof arg === 'undefined') { return `${arg}`; } if (typeof arg === 'string') { return `"${arg}"`; } if (Array.isArray(arg)) { try { const res = JSON.stringify(arg); return res; } catch (err) { return `[${arg.toString()}]`; } } if (typeof arg === 'function') { return arg.toString(); } if (arg instanceof Secret) { return '*****'; } if (arg.toString && arg.toString() !== '[object Object]') { return arg.toString(); } 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().replace(global.codecept_dir || '', '.').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; } /** @return {boolean} */ isBDD() { if (this.actor && this.actor.match && this.actor.match(/^(Given|When|Then|And)/)) { return true; } return false; } isWithin() { if (this.actor && this.actor.match && this.actor.match(/^(Within)/)) { return true; } return false; } toString() { const actorText = !this.isBDD() && !this.isWithin() ? `${this.actor}:` : this.actor; return `${this.prefix}${actorText} ${this.humanize()} ${this.humanizeArgs()}${this.suffix}`; } humanize() { return this.name; } setTrace() { } setContext(context) { this.context = context; } /** @return {*} */ run(fn) { this.status = 'queued'; this.setArguments(Array.from(arguments).slice(1)); let result; const registerStep = (step) => { step.metaStep = this; }; event.dispatcher.on(event.step.before, registerStep); try { this.startTime = Date.now(); result = fn.apply(this.context, this.args); } catch (error) { this.status = 'failed'; } finally { this.endTime = Date.now(); event.dispatcher.removeListener(event.step.before, registerStep); } return result; } } /** @type {Class<MetaStep>} */ Step.MetaStep = MetaStep; module.exports = Step; function dryRunResolver() { return { get(target, prop) { if (prop === 'toString') return () => '<VALUE>'; return new Proxy({}, dryRunResolver()); }, }; }