@revoloo/cypress6
Version:
Cypress.io end to end testing tool
556 lines (422 loc) • 14.5 kB
JavaScript
/* eslint-disable prefer-rest-params */
const _ = require('lodash')
const $errUtils = require('./error_utils')
const { getTestFromRunnable } = require('./utils')
const $stackUtils = require('./stack_utils')
// in the browser mocha is coming back
// as window
const mocha = require('mocha')
const Mocha = mocha.Mocha != null ? mocha.Mocha : mocha
const { Test, Runner, Runnable, Hook, Suite } = Mocha
const runnerRun = Runner.prototype.run
const runnerFail = Runner.prototype.fail
const runnerRunTests = Runner.prototype.runTests
const runnableRun = Runnable.prototype.run
const runnableClearTimeout = Runnable.prototype.clearTimeout
const runnableResetTimeout = Runnable.prototype.resetTimeout
const testRetries = Test.prototype.retries
const testClone = Test.prototype.clone
const suiteAddTest = Suite.prototype.addTest
const suiteAddSuite = Suite.prototype.addSuite
const suiteRetries = Suite.prototype.retries
const hookRetries = Hook.prototype.retries
const suiteBeforeAll = Suite.prototype.beforeAll
const suiteBeforeEach = Suite.prototype.beforeEach
const suiteAfterAll = Suite.prototype.afterAll
const suiteAfterEach = Suite.prototype.afterEach
// don't let mocha pollute the global namespace
delete window.mocha
delete window.Mocha
function invokeFnWithOriginalTitle (ctx, originalTitle, mochaArgs, fn, _testConfig) {
const ret = fn.apply(ctx, mochaArgs)
ret._testConfig = _testConfig
ret.originalTitle = originalTitle
return ret
}
function overloadMochaFnForConfig (fnName, specWindow) {
const _fn = specWindow[fnName]
const fnType = fnName === 'it' || fnName === 'specify' ? 'Test' : 'Suite'
function overrideFn (fn) {
specWindow[fnName] = fn()
specWindow[fnName]['only'] = fn('only')
specWindow[fnName]['skip'] = fn('skip')
// override xit, xdescribe, etc
if (specWindow[`x${fnName}`]) specWindow[`x${fnName}`] = specWindow[fnName]['skip']
}
overrideFn(function (subFn) {
return function (...args) {
/**
* @type {Cypress.Cypress}
*/
const Cypress = specWindow.Cypress
const origFn = subFn ? _fn[subFn] : _fn
if (args.length > 2 && _.isObject(args[1])) {
const _testConfig = _.extend({}, args[1])
const mochaArgs = [args[0], args[2]]
const configMatchesBrowser = _testConfig.browser == null || Cypress.isBrowser(_testConfig.browser, `${fnType} config value \`{ browser }\``)
if (!configMatchesBrowser) {
// TODO: this would mess up the dashboard since it would be registered as a new test
const originalTitle = mochaArgs[0]
mochaArgs[0] = `${originalTitle} (skipped due to browser)`
// TODO: weird edge case where you have an .only but also skipped the test due to the browser
if (subFn === 'only') {
mochaArgs[1] = function () {
this.skip()
}
return invokeFnWithOriginalTitle(this, originalTitle, mochaArgs, origFn, _testConfig)
}
return invokeFnWithOriginalTitle(this, originalTitle, mochaArgs, _fn['skip'], _testConfig)
}
const ret = origFn.apply(this, mochaArgs)
ret._testConfig = _testConfig
return ret
}
return origFn.apply(this, args)
}
})
}
const getInvocationDetails = (specWindow, config) => {
if (specWindow.Error) {
let stack = (new specWindow.Error()).stack
// note: specWindow.Cypress can be undefined or null
// if the user quickly reloads the tests multiple times
// firefox throws a different stack than chromium
// which includes stackframes from cypress_runner.js.
// So we drop the lines until we get to the spec stackframe (incldues __cypress/tests)
if (specWindow.Cypress && specWindow.Cypress.isBrowser('firefox')) {
stack = $stackUtils.stackWithLinesDroppedFromMarker(stack, '__cypress/tests', true)
}
const details = $stackUtils.getSourceDetailsForFirstLine(stack, config('projectRoot'))
return {
details,
stack,
}
}
}
const ui = (specWindow, _mocha) => {
// Override mocha.ui so that the pre-require event is emitted
// with the iframe's `window` reference, rather than the parent's.
_mocha.ui = function (name) {
this._ui = Mocha.interfaces[name]
if (!this._ui) {
$errUtils.throwErrByPath('mocha.invalid_interface', { args: { name } })
}
this._ui = this._ui(this.suite)
// this causes the mocha globals in the spec window to be defined
// such as describe, it, before, beforeEach, etc
this.suite.emit('pre-require', specWindow, null, this)
// allow testConfigOverrides/suite-config-overrides
// by accepting 3 arguments to it/describe/context
overloadMochaFnForConfig('it', specWindow)
overloadMochaFnForConfig('specify', specWindow)
overloadMochaFnForConfig('describe', specWindow)
overloadMochaFnForConfig('context', specWindow)
return this
}
return _mocha.ui('bdd')
}
const setMochaProps = (specWindow, _mocha) => {
// Mocha is usually defined in the spec when used normally
// in the browser or node, so we add it as a global
// for our users too
const M = (specWindow.Mocha = Mocha)
const m = (specWindow.mocha = _mocha)
// also attach the Mocha class
// to the mocha instance for clarity
m.Mocha = M
// this needs to be part of the configuration of cypress.json
// we can't just forcibly use bdd
return ui(specWindow, _mocha)
}
const createMocha = (specWindow) => {
const _mocha = new Mocha({
reporter: () => {},
timeout: false,
})
// set mocha props on the specWindow
setMochaProps(specWindow, _mocha)
// return the newly created mocha instance
return _mocha
}
const getRunner = function (_mocha) {
Runner.prototype.run = function () {
// reset our runner#run function
// so the next time we call it
// its normal again!
restoreRunnerRun()
// return the runner instance
return this
}
return _mocha.run()
}
const restoreRunnableClearTimeout = () => {
Runnable.prototype.clearTimeout = runnableClearTimeout
}
const restoreRunnableResetTimeout = () => {
Runnable.prototype.resetTimeout = runnableResetTimeout
}
const restoreRunnerRun = () => {
Runner.prototype.run = runnerRun
}
const restoreRunnerFail = () => {
Runner.prototype.fail = runnerFail
}
const restoreRunnableRun = () => {
Runnable.prototype.run = runnableRun
}
const restoreSuiteRetries = () => {
Suite.prototype.retries = suiteRetries
}
const restoreTestClone = () => {
Test.prototype.clone = testClone
}
const restoreRunnerRunTests = () => {
Runner.prototype.runTests = runnerRunTests
}
const restoreSuiteAddTest = () => {
Suite.prototype.addTest = suiteAddTest
}
const restoreSuiteAddSuite = () => {
Suite.prototype.addSuite = suiteAddSuite
}
const restoreHookRetries = () => {
Hook.prototype.retries = hookRetries
}
const restoreSuiteHooks = () => {
Suite.prototype.beforeAll = suiteBeforeAll
Suite.prototype.beforeEach = suiteBeforeEach
Suite.prototype.afterAll = suiteAfterAll
Suite.prototype.afterEach = suiteAfterEach
}
const patchSuiteRetries = () => {
Suite.prototype.retries = function (...args) {
if (args[0] !== undefined && args[0] > -1) {
const err = $errUtils.cypressErrByPath('mocha.manually_set_retries_suite', {
args: {
title: this.title,
numRetries: args[0] ?? 2,
},
})
throw err
}
return suiteRetries.apply(this, args)
}
}
const patchHookRetries = () => {
Hook.prototype.retries = function (...args) {
if (args[0] !== undefined && args[0] > -1) {
const err = $errUtils.cypressErrByPath('mocha.manually_set_retries_suite', {
args: {
title: this.parent.title,
numRetries: args[0] ?? 2,
},
})
// so this error doesn't cause a retry
getTestFromRunnable(this)._retries = -1
throw err
}
return hookRetries.apply(this, args)
}
}
// matching the current Runner.prototype.fail except
// changing the logic for determing whether this is a valid err
const patchRunnerFail = () => {
Runner.prototype.fail = function (runnable, err) {
const errMessage = _.get(err, 'message')
if (errMessage && errMessage.indexOf('Resolution method is overspecified') > -1) {
err.message = $errUtils.errByPath('mocha.overspecified', { error: err.stack }).message
}
// if this isnt a correct error object then just bail
// and call the original function
if (Object.prototype.toString.call(err) !== '[object Error]') {
return runnerFail.call(this, runnable, err)
}
// else replicate the normal mocha functionality
++this.failures
runnable.state = 'failed'
this.emit('fail', runnable, err)
}
}
const patchRunnableRun = (Cypress) => {
Runnable.prototype.run = function (...args) {
const runnable = this
Cypress.action('mocha:runnable:run', runnableRun, runnable, args)
}
}
function patchTestClone () {
Test.prototype.clone = function () {
if (this._retriesBeforeEachFailedTestFn) {
this.fn = this._retriesBeforeEachFailedTestFn
}
const ret = testClone.apply(this, arguments)
// carry over testConfigOverrides
ret._testConfig = this._testConfig
// carry over test.id
ret.id = this.id
return ret
}
}
function patchRunnerRunTests () {
Runner.prototype.runTests = function () {
const suite = arguments[0]
const _slice = suite.tests.slice
// HACK: we need to dynamically enqueue tests to suite.tests during a test run
// however Mocha calls `.slice` on this property and thus we no longer have a reference
// to the internal test queue. So we replace the .slice method
// in a way that we keep a reference to the returned array. we name it suite.testsQueue
suite.tests.slice = function () {
this.slice = _slice
const ret = _slice.apply(this, arguments)
suite.testsQueue = ret
return ret
}
const ret = runnerRunTests.apply(this, arguments)
return ret
}
}
const patchRunnableClearTimeout = () => {
Runnable.prototype.clearTimeout = function (...args) {
// call the original
runnableClearTimeout.apply(this, args)
this.timer = null
}
}
const patchSuiteAddTest = (specWindow, config) => {
Suite.prototype.addTest = function (...args) {
const test = args[0]
if (!test.invocationDetails) {
test.invocationDetails = getInvocationDetails(specWindow, config).details
}
const ret = suiteAddTest.apply(this, args)
test.retries = function (...args) {
if (args[0] !== undefined && args[0] > -1) {
const err = $errUtils.cypressErrByPath('mocha.manually_set_retries_test', {
args: {
title: test.title,
numRetries: args[0] ?? 2,
},
})
// so this error doesn't cause a retry
test._retries = -1
throw err
}
return testRetries.apply(this, args)
}
return ret
}
}
const patchSuiteAddSuite = (specWindow, config) => {
Suite.prototype.addSuite = function (...args) {
const suite = args[0]
if (!suite.invocationDetails) {
suite.invocationDetails = getInvocationDetails(specWindow, config).details
}
return suiteAddSuite.apply(this, args)
}
}
const patchRunnableResetTimeout = () => {
Runnable.prototype.resetTimeout = function () {
const runnable = this
const ms = this.timeout() || 1e9
this.clearTimeout()
const getErrPath = function () {
// we've yield an explicit done callback
if (runnable.async) {
return 'mocha.async_timed_out'
}
// TODO: improve this error message. It's not that
// a command necessarily timed out - in fact this is
// a mocha timeout, and a command likely *didn't*
// time out correctly, so we received this message instead.
return 'mocha.timed_out'
}
this.timer = setTimeout(() => {
const err = $errUtils.errByPath(getErrPath(), { ms })
runnable.callback(err)
runnable.timedOut = true
}, ms)
}
}
const patchSuiteHooks = (specWindow, config) => {
_.each(['beforeAll', 'beforeEach', 'afterAll', 'afterEach'], (fnName) => {
const _fn = Suite.prototype[fnName]
Suite.prototype[fnName] = function (title, fn) {
const _createHook = this._createHook
this._createHook = function (title, fn) {
const hook = _createHook.call(this, title, fn)
let invocationStack = hook.invocationDetails?.stack
if (!hook.invocationDetails) {
const invocationDetails = getInvocationDetails(specWindow, config)
hook.invocationDetails = invocationDetails.details
invocationStack = invocationDetails.stack
}
if (this._condensedHooks) {
throw $errUtils.errByPath('mocha.hook_registered_late', { hookTitle: fnName })
.setUserInvocationStack(invocationStack)
}
return hook
}
const ret = _fn.call(this, title, fn)
this._createHook = _createHook
return ret
}
})
}
const restore = () => {
restoreRunnerRun()
restoreRunnerFail()
restoreRunnableRun()
restoreRunnableClearTimeout()
restoreRunnableResetTimeout()
restoreSuiteRetries()
restoreHookRetries()
restoreRunnerRunTests()
restoreTestClone()
restoreSuiteAddTest()
restoreSuiteAddSuite()
restoreSuiteHooks()
}
const override = (specWindow, Cypress, config) => {
patchRunnerFail()
patchRunnableRun(Cypress)
patchRunnableClearTimeout()
patchRunnableResetTimeout()
patchSuiteRetries()
patchHookRetries()
patchRunnerRunTests()
patchTestClone()
patchSuiteAddTest(specWindow, config)
patchSuiteAddSuite(specWindow, config)
patchSuiteHooks(specWindow, config)
}
const create = (specWindow, Cypress, config) => {
restore()
override(specWindow, Cypress, config)
// generate the mocha + Mocha globals
// on the specWindow, and get the new
// _mocha instance
const _mocha = createMocha(specWindow)
const _runner = getRunner(_mocha)
_mocha.suite.file = Cypress.spec.relative
return {
_mocha,
createRootTest (title, fn) {
const r = new Test(title, fn)
_runner.suite.addTest(r)
return r
},
createTest (title, fn) {
return new Test(title, fn)
},
getRunner () {
return _runner
},
getRootSuite () {
return _mocha.suite
},
}
}
module.exports = {
restore,
create,
}