mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-05 06:20:44 -05:00
b0c0eaa508
Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com> Co-authored-by: Zach Bloomquist <git@chary.us> Co-authored-by: Tyler Biethman <tbiethman@users.noreply.github.com> Co-authored-by: Matt Henkes <mjhenkes@gmail.com> Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com> Co-authored-by: Matt Schile <mschile@cypress.io> Co-authored-by: Mark Noonan <mark@cypress.io> Co-authored-by: Zachary Williams <ZachJW34@gmail.com> Co-authored-by: Ben M <benm@cypress.io> Co-authored-by: Zachary Williams <zachjw34@gmail.com> Co-authored-by: astone123 <adams@cypress.io> Co-authored-by: Bill Glesias <bglesias@gmail.com> Co-authored-by: Emily Rohrbough <emilyrohrbough@yahoo.com> Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net> Co-authored-by: Adam Stone <adams@cypress.io> Co-authored-by: Blue F <blue@cypress.io> Co-authored-by: GitStart <1501599+gitstart@users.noreply.github.com> Co-authored-by: Mike Plummer <mike-plummer@users.noreply.github.com> Co-authored-by: Jordan <jordan@jpdesigning.com> Co-authored-by: Sam Goodger <turbo@tailz.dev> Co-authored-by: Colum Ferry <cferry09@gmail.com> Co-authored-by: Stokes Player <stokes@cypress.io> Co-authored-by: Vilhelm Melkstam <vilhelm.melkstam@gmail.com> Co-authored-by: amehta265 <65267668+amehta265@users.noreply.github.com>
559 lines
15 KiB
JavaScript
559 lines
15 KiB
JavaScript
const _ = require('lodash')
|
|
const path = require('path')
|
|
const { stackUtils } = require('@packages/errors')
|
|
// mocha-* is used to allow us to have later versions of mocha specified in devDependencies
|
|
// and prevents accidentally 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 mochaSymbols = mochaReporters.Base.symbols
|
|
|
|
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 getTitlePath = function (runnable, titles = []) {
|
|
// `originalTitle` is a Mocha Hook concept used to associated the
|
|
// hook to the test that executed it
|
|
if (runnable.originalTitle) {
|
|
runnable.title = runnable.originalTitle
|
|
}
|
|
|
|
if (runnable.title) {
|
|
// sanitize the title which may have been altered by a suite-/
|
|
// test-level browser skip to ensure the original title is used
|
|
const BROWSER_SKIP_TITLE = ' (skipped due to browser)'
|
|
|
|
titles.unshift(runnable.title.replace(BROWSER_SKIP_TITLE, ''))
|
|
}
|
|
|
|
if (runnable.parent) {
|
|
return getTitlePath(runnable.parent, 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
|
|
}
|
|
|
|
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,
|
|
})
|
|
|
|
if (this.reporterName === 'spec') {
|
|
// Unfortunately the reporter doesn't expose its indentation logic, so we have to replicate it here
|
|
let indents = 0
|
|
|
|
this.runner.on('suite', function (suite) {
|
|
++indents
|
|
})
|
|
|
|
this.runner.on('suite end', function () {
|
|
--indents
|
|
})
|
|
|
|
// Override the default reporter to always show test timing even for fast tests
|
|
// and display slow ones in yellow rather than red
|
|
this.runner._events.pass[2] = function (test) {
|
|
const durationColor = test.speed === 'slow' ? 'medium' : 'fast'
|
|
const fmt =
|
|
Array(indents).join(' ') +
|
|
mochaColor('checkmark', ` ${ mochaSymbols.ok}`) +
|
|
mochaColor('pass', ' %s') +
|
|
mochaColor(durationColor, ' (%dms)')
|
|
|
|
// Log: `✓ test title (300ms)` when a test passes
|
|
// eslint-disable-next-line no-console
|
|
console.log(fmt, test.title, test.duration)
|
|
}
|
|
}
|
|
|
|
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: getTitlePath(hook),
|
|
body: hook.body,
|
|
}
|
|
}
|
|
|
|
normalizeTest (test = {}) {
|
|
const normalizedTest = {
|
|
testId: orNull(test.id),
|
|
title: getTitlePath(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: attempt.err.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
|
|
|
|
debug('trying to load reporter:', reporterName)
|
|
|
|
// Explicitly require this here (rather than dynamically) so that it gets included in the v8 snapshot
|
|
if (reporterName === 'teamcity') {
|
|
debug(`${reporterName} is built-in reporter`)
|
|
|
|
return require('mocha-teamcity-reporter')
|
|
}
|
|
|
|
// Explicitly require this here (rather than dynamically) so that it gets included in the v8 snapshot
|
|
if (reporterName === 'junit') {
|
|
debug(`${reporterName} is built-in reporter`)
|
|
|
|
return require('mocha-junit-reporter')
|
|
}
|
|
|
|
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
|