UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

537 lines (418 loc) 13.7 kB
const _ = require('lodash') const path = require('path') const stackUtils = require('./util/stack_utils') // mocha-* is used to allow us to have later versions of mocha specified in devDependencies // and prevents accidently upgrading this one // TODO: look into upgrading this to version in driver const Mocha = require('mocha-7.0.1') const mochaReporters = require('mocha-7.0.1/lib/reporters') const mochaCreateStatsCollector = require('mocha-7.0.1/lib/stats-collector') const mochaColor = mochaReporters.Base.color const debug = require('debug')('cypress:server:reporter') const Promise = require('bluebird') const { overrideRequire } = require('./override_require') // override calls to `require('mocha*')` when to always resolve with a mocha we control // otherwise mocha will be resolved from project's node_modules and might not work with our code const customReporterMochaPath = path.dirname(require.resolve('mocha-7.0.1')) overrideRequire((depPath, _load) => { if ((depPath === 'mocha') || depPath.startsWith('mocha/')) { return _load(depPath.replace('mocha', customReporterMochaPath)) } }) // if Mocha.Suite.prototype.titlePath // throw new Error('Mocha.Suite.prototype.titlePath already exists. Please remove the monkeypatch code.') // Mocha.Suite.prototype.titlePath = -> // result = [] // if @parent // result = result.concat(@parent.titlePath()) // if !@root // result.push(@title) // return result // Mocha.Runnable.prototype.titlePath = -> // @parent.titlePath().concat([@title]) const getParentTitle = function (runnable, titles) { // if the browser/reporter changed the runnable title (for display purposes) // it will have .originalTitle which is the name of the test before title change let p if (runnable.originalTitle) { runnable.title = runnable.originalTitle } if (!titles) { titles = [runnable.title] } p = runnable.parent if (p) { let t t = p.title if (t) { titles.unshift(t) } return getParentTitle(p, titles) } return titles } const createSuite = function (obj, parent) { const suite = new Mocha.Suite(obj.title, {}) if (parent) { suite.parent = parent } suite.file = obj.file suite.root = !!obj.root return suite } const createRunnable = function (obj, parent) { let fn const { body } = obj if (body) { fn = function () {} fn.toString = () => { return body } } const runnable = new Mocha.Test(obj.title, fn) runnable.timedOut = obj.timedOut runnable.async = obj.async runnable.sync = obj.sync runnable.duration = obj.duration runnable.state = obj.state != null ? obj.state : 'skipped' // skipped by default runnable._retries = obj._retries // shouldn't need to set _currentRetry, but we'll do it anyways runnable._currentRetry = obj._currentRetry if (runnable.body == null) { runnable.body = body } if (parent) { runnable.parent = parent } return runnable } const mochaProps = { 'currentRetry': '_currentRetry', 'retries': '_retries', } const toMochaProps = (testProps) => { return _.each(mochaProps, (val, key) => { if (testProps.hasOwnProperty(key)) { testProps[val] = testProps[key] return delete testProps[key] } }) } const toAttemptProps = (runnable) => { return _.pick(runnable, [ 'err', 'state', 'timings', 'failedFromHookId', 'wallClockStartedAt', 'wallClockDuration', ]) } const mergeRunnable = (eventName) => { return (function (testProps, runnables) { toMochaProps(testProps) const runnable = runnables[testProps.id] if (eventName === 'test:before:run') { if (testProps._currentRetry > runnable._currentRetry) { debug('test retried:', testProps.title) const prevAttempts = runnable.prevAttempts || [] delete runnable.prevAttempts const prevAttempt = toAttemptProps(runnable) delete runnable.failedFromHookId delete runnable.err delete runnable.hookName testProps.prevAttempts = prevAttempts.concat([prevAttempt]) } } return _.extend(runnable, testProps) }) } const safelyMergeRunnable = function (hookProps, runnables) { const { hookId, title, hookName, body, type } = hookProps if (!runnables[hookId]) { runnables[hookId] = { hookId, type, title, body, hookName, } } return _.extend({}, runnables[hookProps.id], hookProps) } const mergeErr = function (runnable, runnables, stats) { // this will always be a test because // we reset hook id's to match tests let test = runnables[runnable.id] test.err = runnable.err test.state = 'failed' if (runnable.type === 'hook') { test.failedFromHookId = runnable.hookId } // dont mutate the test, and merge in the runnable title // in the case its a hook so that we emit the right 'fail' // event for reporters test = _.extend({}, test, { title: runnable.title }) return [test, test.err] } const setDate = function (obj, runnables, stats) { let e; let s s = obj.start if (s) { stats.wallClockStartedAt = new Date(s) } e = obj.end if (e) { stats.wallClockEndedAt = new Date(e) } return null } const orNull = function (prop) { if (prop == null) return null return prop } const events = { 'start': setDate, 'end': setDate, 'suite': mergeRunnable('suite'), 'suite end': mergeRunnable('suite end'), 'test': mergeRunnable('test'), 'test end': mergeRunnable('test end'), 'hook': safelyMergeRunnable, 'retry': true, 'hook end': safelyMergeRunnable, 'pass': mergeRunnable('pass'), 'pending': mergeRunnable('pending'), 'fail': mergeErr, 'test:after:run': mergeRunnable('test:after:run'), // our own custom event 'test:before:run': mergeRunnable('test:before:run'), // our own custom event } const reporters = { teamcity: 'mocha-teamcity-reporter', junit: 'mocha-junit-reporter', } class Reporter { constructor (reporterName = 'spec', reporterOptions = {}, projectRoot) { if (!(this instanceof Reporter)) { return new Reporter(reporterName) } this.reporterName = reporterName this.projectRoot = projectRoot this.reporterOptions = reporterOptions this.normalizeTest = this.normalizeTest.bind(this) } setRunnables (rootRunnable) { if (!rootRunnable) { rootRunnable = { title: '' } } // manage stats ourselves this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, skipped: 0, failures: 0 } this.runnables = {} rootRunnable = this._createRunnable(rootRunnable, 'suite') const reporter = Reporter.loadReporter(this.reporterName, this.projectRoot) this.mocha = new Mocha({ reporter }) this.mocha.suite = rootRunnable this.runner = new Mocha.Runner(rootRunnable) mochaCreateStatsCollector(this.runner) if (this.reporterName === 'spec') { this.runner.on('retry', (test) => { const runnable = this.runnables[test.id] const padding = ' '.repeat(runnable.titlePath().length) const retryMessage = mochaColor('medium', `(Attempt ${test.currentRetry + 1} of ${test.retries + 1})`) // Log: `(Attempt 1 of 2) test title` when a test retries // eslint-disable-next-line no-console return console.log(`${padding}${retryMessage} ${test.title}`) }) } this.reporter = new this.mocha._reporter(this.runner, { reporterOptions: this.reporterOptions, }) this.runner.ignoreLeaks = true } _createRunnable (runnableProps, type, parent) { const runnable = (() => { switch (type) { case 'suite': // eslint-disable-next-line no-case-declarations const suite = createSuite(runnableProps, parent) suite.tests = _.map(runnableProps.tests, (testProps) => { return this._createRunnable(testProps, 'test', suite) }) suite.suites = _.map(runnableProps.suites, (suiteProps) => { return this._createRunnable(suiteProps, 'suite', suite) }) return suite case 'test': return createRunnable(runnableProps, parent) default: throw new Error(`Unknown runnable type: '${type}'`) } })() runnable.id = runnableProps.id this.runnables[runnableProps.id] = runnable return runnable } emit (event, ...args) { args = this.parseArgs(event, args) if (args) { return this.runner && this.runner.emit.apply(this.runner, args) } } parseArgs (event, args) { // make sure this event is in our events hash let e e = events[event] if (e) { if (_.isFunction(e)) { debug('got mocha event \'%s\' with args: %o', event, args) // transform the arguments if // there is an event.fn callback args = e.apply(this, args.concat(this.runnables, this.stats)) } return [event].concat(args) } } normalizeHook (hook = {}) { return { hookId: hook.hookId, hookName: hook.hookName, title: getParentTitle(hook), body: hook.body, } } normalizeTest (test = {}) { const normalizedTest = { testId: orNull(test.id), title: getParentTitle(test), state: orNull(test.state), body: orNull(test.body), displayError: orNull(test.err && test.err.stack), attempts: _.map((test.prevAttempts || []).concat([test]), (attempt) => { const err = attempt.err && { name: attempt.err.name, message: attempt.err.message, stack: stackUtils.stackWithoutMessage(attempt.err.stack), codeFrame: attempt.err.codeFrame, } return { state: orNull(attempt.state), error: orNull(err), timings: orNull(attempt.timings), failedFromHookId: orNull(attempt.failedFromHookId), wallClockStartedAt: orNull(attempt.wallClockStartedAt && new Date(attempt.wallClockStartedAt)), wallClockDuration: orNull(attempt.wallClockDuration), videoTimestamp: null, } }), } return normalizedTest } end () { if (this.reporter.done) { const { failures, } = this.runner return new Promise((resolve, reject) => { return this.reporter.done(failures, resolve) }).then(() => { return this.results() }) } return this.results() } results () { const tests = _ .chain(this.runnables) .filter({ type: 'test' }) .map(this.normalizeTest) .value() const hooks = _ .chain(this.runnables) .filter({ type: 'hook' }) .map(this.normalizeHook) .value() const suites = _ .chain(this.runnables) .filter({ root: false }) // don't include root suite .value() // default to 0 this.stats.wallClockDuration = 0 const { wallClockStartedAt, wallClockEndedAt } = this.stats if (wallClockStartedAt && wallClockEndedAt) { this.stats.wallClockDuration = wallClockEndedAt - wallClockStartedAt } this.stats.suites = suites.length this.stats.tests = tests.length this.stats.passes = _.filter(tests, { state: 'passed' }).length this.stats.pending = _.filter(tests, { state: 'pending' }).length this.stats.skipped = _.filter(tests, { state: 'skipped' }).length this.stats.failures = _.filter(tests, { state: 'failed' }).length // return an object of results return { // this is our own stats object stats: this.stats, reporter: this.reporterName, // this comes from the reporter, not us reporterStats: this.runner.stats, hooks, tests, } } static setVideoTimestamp (videoStart, tests = []) { return _.map(tests, (test) => { // if we have a wallClockStartedAt let wcs wcs = test.wallClockStartedAt if (wcs) { test.videoTimestamp = test.wallClockStartedAt - videoStart } return test }) } static create (reporterName, reporterOptions, projectRoot) { return new Reporter(reporterName, reporterOptions, projectRoot) } static loadReporter (reporterName, projectRoot) { let p; let r debug('trying to load reporter:', reporterName) r = reporters[reporterName] if (r) { debug(`${reporterName} is built-in reporter`) return require(r) } if (mochaReporters[reporterName]) { debug(`${reporterName} is Mocha reporter`) return reporterName } // it's likely a custom reporter // that is local (./custom-reporter.js) // or one installed by the user through npm try { p = path.resolve(projectRoot, reporterName) // try local debug('trying to require local reporter with path:', p) // using path.resolve() here so we can just pass an // absolute path as the reporterName which avoids // joining projectRoot unnecessarily return require(p) } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { // bail early if the error wasn't MODULE_NOT_FOUND // because that means theres something actually wrong // with the found reporter throw err } p = path.resolve(projectRoot, 'node_modules', reporterName) // try npm. if this fails, we're out of options, so let it throw debug('trying to require local reporter with path:', p) return require(p) } } static getSearchPathsForReporter (reporterName, projectRoot) { return _.uniq([ path.resolve(projectRoot, reporterName), path.resolve(projectRoot, 'node_modules', reporterName), ]) } } module.exports = Reporter