From 1d9d61f18532052affe33942b6299ff3619f6e76 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 20 Feb 2019 18:19:35 +0630 Subject: [PATCH 001/300] Add 'attempts' component - Style attempt headers in new scss file - Create collapsible space within attempt to nest hooks --- packages/reporter/src/attempts/attempts.jsx | 48 ++++++++++++++++++++ packages/reporter/src/attempts/attempts.scss | 34 ++++++++++++++ packages/reporter/src/test/test.jsx | 24 +++++----- 3 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 packages/reporter/src/attempts/attempts.jsx create mode 100644 packages/reporter/src/attempts/attempts.scss diff --git a/packages/reporter/src/attempts/attempts.jsx b/packages/reporter/src/attempts/attempts.jsx new file mode 100644 index 000000000000..5eb9b6d586db --- /dev/null +++ b/packages/reporter/src/attempts/attempts.jsx @@ -0,0 +1,48 @@ +import cs from 'classnames' +import _ from 'lodash' +import { observer } from 'mobx-react' +import React from 'react' +import Collapsible from '../collapsible/collapsible' +import Hooks from '../hooks/hooks' + +const NoCommands = observer(() => ( + +)) + +const AttemptHeader = ({ attempt, retriesCount }) => ( + + + {attempt.id}/{retriesCount} + + + + +) + +const Attempt = observer(({ model, attempt, retriesCount }) => ( +
  • + } + headerClass='attempt-name' + isOpen={true} + > +
      + {model.commands.length ? : } +
    +
    +
  • +)) + +const Attempts = observer(({ model, attempts, retriesCount }) => ( + +)) + +export { Attempt, AttemptHeader, NoCommands } + +export default Attempts diff --git a/packages/reporter/src/attempts/attempts.scss b/packages/reporter/src/attempts/attempts.scss new file mode 100644 index 000000000000..98c58f5554ab --- /dev/null +++ b/packages/reporter/src/attempts/attempts.scss @@ -0,0 +1,34 @@ +.reporter .runnable.suite .attempt-name { + border-top: 1px solid #bbb; + margin: 20px 0 30px; + height: 0; + position: relative; + + .attempt-tag { + border: 1px solid #bbb; + border-radius: 10px; + padding: 2px 1px; + background-color: #fff; + position: absolute; + top: -12px; + right: 10px; + + &:hover { + background-color: #bbb; + color: #fff; + i { + color: #fff; + } + } + } + + .collapsible-indicator { + display: none; + } + + .hook-failed-message { + i { + color: $fail; + } + } +} \ No newline at end of file diff --git a/packages/reporter/src/test/test.jsx b/packages/reporter/src/test/test.jsx index 9faf3301f9cf..021dc1b918e3 100644 --- a/packages/reporter/src/test/test.jsx +++ b/packages/reporter/src/test/test.jsx @@ -10,19 +10,11 @@ import { indent } from '../lib/util' import runnablesStore from '../runnables/runnables-store' import scroller from '../lib/scroller' -import Hooks from '../hooks/hooks' +import Attempts from '../attempts/attempts' import Agents from '../agents/agents' import Routes from '../routes/routes' import FlashOnClick from '../lib/flash-on-click' -const NoCommands = observer(() => ( - -)) - @observer class Test extends Component { static defaultProps = { @@ -95,6 +87,16 @@ class Test extends Component { const { model } = this.props + const retriesCount = 3 + const attempts = [ + { + id: 1, + }, + { + id: 2, + }, + ] + return (
    - {model.commands.length ? : } + {retriesCount ? : null}
    ) @@ -137,6 +139,4 @@ class Test extends Component { } } -export { NoCommands } - export default Test From 1a844209522d46bd6de4c06c6f54b6299fab090b Mon Sep 17 00:00:00 2001 From: decaffeinate <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:29:23 -0500 Subject: [PATCH 002/300] decaffeinate: Rename runner.coffee from .coffee to .js --- packages/driver/src/cypress/{runner.coffee => runner.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/driver/src/cypress/{runner.coffee => runner.js} (100%) diff --git a/packages/driver/src/cypress/runner.coffee b/packages/driver/src/cypress/runner.js similarity index 100% rename from packages/driver/src/cypress/runner.coffee rename to packages/driver/src/cypress/runner.js From 96dd36059c957e7ac0ff8ac7610aba7a9fdac501 Mon Sep 17 00:00:00 2001 From: decaffeinate <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:29:26 -0500 Subject: [PATCH 003/300] decaffeinate: Convert runner.coffee to JS --- packages/driver/src/cypress/runner.js | 2119 +++++++++++++------------ 1 file changed, 1139 insertions(+), 980 deletions(-) diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index 15a96379d89c..b438a652218e 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -1,441 +1,518 @@ -_ = require("lodash") -moment = require("moment") -Promise = require("bluebird") -Pending = require("mocha/lib/pending") - -$Log = require("./log") -$utils = require("./utils") - -defaultGrepRe = /.*/ -mochaCtxKeysRe = /^(_runnable|test)$/ -betweenQuotesRe = /\"(.+?)\"/ - -HOOKS = "beforeAll beforeEach afterEach afterAll".split(" ") -TEST_BEFORE_RUN_EVENT = "runner:test:before:run" -TEST_AFTER_RUN_EVENT = "runner:test:after:run" - -ERROR_PROPS = "message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending".split(" ") -RUNNABLE_LOGS = "routes agents commands".split(" ") -RUNNABLE_PROPS = "id title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings".split(" ") - -# ## initial payload -# { -# suites: [ -# {id: "r1"}, {id: "r4", suiteId: "r1"} -# ] -# tests: [ -# {id: "r2", title: "foo", suiteId: "r1"} -# ] -# } - -# ## normalized -# { -# { -# root: true -# suites: [] -# tests: [] -# } -# } - -# ## resetting state (get back from server) -# { -# scrollTop: 100 -# tests: { -# r2: {id: "r2", title: "foo", suiteId: "r1", state: "passed", err: "", routes: [ -# {}, {} -# ] -# agents: [ -# ] -# commands: [ -# {}, {}, {} -# ] -# }} -# -# r3: {id: "r3", title: "bar", suiteId: "r1", state: "failed", logs: { -# routes: [ -# {}, {} -# ] -# spies: [ -# ] -# commands: [ -# {}, {}, {} -# ] -# }} -# ] -# } - -fire = (event, runnable, Cypress) -> - runnable._fired ?= {} - runnable._fired[event] = true - - ## dont fire anything again if we are skipped - return if runnable._ALREADY_RAN - - Cypress.action(event, wrap(runnable), runnable) - -fired = (event, runnable) -> - !!(runnable._fired and runnable._fired[event]) - -testBeforeRunAsync = (test, Cypress) -> - Promise.try -> - if not fired("runner:test:before:run:async", test) - fire("runner:test:before:run:async", test, Cypress) - -runnableAfterRunAsync = (runnable, Cypress) -> - Promise.try -> - if not fired("runner:runnable:after:run:async", runnable) - fire("runner:runnable:after:run:async", runnable, Cypress) - -testAfterRun = (test, Cypress) -> - if not fired(TEST_AFTER_RUN_EVENT, test) - setWallClockDuration(test) - fire(TEST_AFTER_RUN_EVENT, test, Cypress) - - ## perf loop only through - ## a tests OWN properties and not - ## inherited properties from its shared ctx - for own key, value of test.ctx - if _.isObject(value) and not mochaCtxKeysRe.test(key) - ## nuke any object properties that come from - ## cy.as() aliases or anything set from 'this' - ## so we aggressively perform GC and prevent obj - ## ref's from building up - test.ctx[key] = undefined - - ## reset the fn to be empty function - ## for GC to be aggressive and prevent - ## closures from hold references - test.fn = -> - - ## prevent loop comprehension - return null - -setTestTimingsForHook = (test, hookName, obj) -> - test.timings ?= {} - test.timings[hookName] ?= [] - test.timings[hookName].push(obj) - -setTestTimings = (test, name, obj) -> - test.timings ?= {} - test.timings[name] = obj - -setWallClockDuration = (test) -> - test.wallClockDuration = new Date() - test.wallClockStartedAt - -reduceProps = (obj, props) -> - _.reduce props, (memo, prop) -> - if _.has(obj, prop) or (obj[prop] isnt undefined) - memo[prop] = obj[prop] - memo - , {} - -wrap = (runnable) -> - ## we need to optimize wrap by converting - ## tests to an id-based object which prevents - ## us from recursively iterating through every - ## parent since we could just return the found test +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS203: Remove `|| {}` from converted for-own loops + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const moment = require("moment"); +const Promise = require("bluebird"); +const Pending = require("mocha/lib/pending"); + +const $Log = require("./log"); +const $utils = require("./utils"); + +const defaultGrepRe = /.*/; +const mochaCtxKeysRe = /^(_runnable|test)$/; +const betweenQuotesRe = /\"(.+?)\"/; + +const HOOKS = "beforeAll beforeEach afterEach afterAll".split(" "); +const TEST_BEFORE_RUN_EVENT = "runner:test:before:run"; +const TEST_AFTER_RUN_EVENT = "runner:test:after:run"; + +const ERROR_PROPS = "message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending".split(" "); +const RUNNABLE_LOGS = "routes agents commands".split(" "); +const RUNNABLE_PROPS = "id title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings".split(" "); + +// ## initial payload +// { +// suites: [ +// {id: "r1"}, {id: "r4", suiteId: "r1"} +// ] +// tests: [ +// {id: "r2", title: "foo", suiteId: "r1"} +// ] +// } + +// ## normalized +// { +// { +// root: true +// suites: [] +// tests: [] +// } +// } + +// ## resetting state (get back from server) +// { +// scrollTop: 100 +// tests: { +// r2: {id: "r2", title: "foo", suiteId: "r1", state: "passed", err: "", routes: [ +// {}, {} +// ] +// agents: [ +// ] +// commands: [ +// {}, {}, {} +// ] +// }} +// +// r3: {id: "r3", title: "bar", suiteId: "r1", state: "failed", logs: { +// routes: [ +// {}, {} +// ] +// spies: [ +// ] +// commands: [ +// {}, {}, {} +// ] +// }} +// ] +// } + +const fire = function(event, runnable, Cypress) { + if (runnable._fired == null) { runnable._fired = {}; } + runnable._fired[event] = true; + + //# dont fire anything again if we are skipped + if (runnable._ALREADY_RAN) { return; } + + return Cypress.action(event, wrap(runnable), runnable); +}; + +const fired = (event, runnable) => !!(runnable._fired && runnable._fired[event]); + +const testBeforeRunAsync = (test, Cypress) => + Promise.try(function() { + if (!fired("runner:test:before:run:async", test)) { + return fire("runner:test:before:run:async", test, Cypress); + } + }) +; + +const runnableAfterRunAsync = (runnable, Cypress) => + Promise.try(function() { + if (!fired("runner:runnable:after:run:async", runnable)) { + return fire("runner:runnable:after:run:async", runnable, Cypress); + } + }) +; + +const testAfterRun = function(test, Cypress) { + if (!fired(TEST_AFTER_RUN_EVENT, test)) { + setWallClockDuration(test); + fire(TEST_AFTER_RUN_EVENT, test, Cypress); + + //# perf loop only through + //# a tests OWN properties and not + //# inherited properties from its shared ctx + for (let key of Object.keys(test.ctx || {})) { + const value = test.ctx[key]; + if (_.isObject(value) && !mochaCtxKeysRe.test(key)) { + //# nuke any object properties that come from + //# cy.as() aliases or anything set from 'this' + //# so we aggressively perform GC and prevent obj + //# ref's from building up + test.ctx[key] = undefined; + } + } + + //# reset the fn to be empty function + //# for GC to be aggressive and prevent + //# closures from hold references + test.fn = function() {}; + + //# prevent loop comprehension + return null; + } +}; + +const setTestTimingsForHook = function(test, hookName, obj) { + if (test.timings == null) { test.timings = {}; } + if (test.timings[hookName] == null) { test.timings[hookName] = []; } + return test.timings[hookName].push(obj); +}; + +const setTestTimings = function(test, name, obj) { + if (test.timings == null) { test.timings = {}; } + return test.timings[name] = obj; +}; + +var setWallClockDuration = test => test.wallClockDuration = new Date() - test.wallClockStartedAt; + +const reduceProps = (obj, props) => + _.reduce(props, function(memo, prop) { + if (_.has(obj, prop) || (obj[prop] !== undefined)) { + memo[prop] = obj[prop]; + } + return memo; + } + , {}) +; + +var wrap = runnable => + //# we need to optimize wrap by converting + //# tests to an id-based object which prevents + //# us from recursively iterating through every + //# parent since we could just return the found test reduceProps(runnable, RUNNABLE_PROPS) +; -wrapAll = (runnable) -> +const wrapAll = runnable => _.extend( {}, reduceProps(runnable, RUNNABLE_PROPS), reduceProps(runnable, RUNNABLE_LOGS) ) +; + +const wrapErr = err => reduceProps(err, ERROR_PROPS); + +const getHookName = function(hook) { + //# find the name of the hook by parsing its + //# title and pulling out whats between the quotes + const name = hook.title.match(betweenQuotesRe); + return name && name[1]; +}; + +const forceGc = function(obj) { + //# aggressively forces GC by purging + //# references to ctx, and removes callback + //# functions for closures + for (let key of Object.keys(obj.ctx || {})) { + const value = obj.ctx[key]; + obj.ctx[key] = undefined; + } + + if (obj.fn) { + return obj.fn = function() {}; + } +}; + +var anyTestInSuite = function(suite, fn) { + for (let test of suite.tests) { + if (fn(test) === true) { return true; } + } + + for (suite of suite.suites) { + if (anyTestInSuite(suite, fn) === true) { return true; } + } + + //# else return false + return false; +}; + +const eachHookInSuite = function(suite, fn) { + for (let type of HOOKS) { + for (let hook of suite[`_${type}`]) { + fn(hook); + } + } + + //# prevent loop comprehension + return null; +}; + +var onFirstTest = function(suite, fn) { + for (var test of suite.tests) { + if (fn(test)) { return test; } + } + + for (suite of suite.suites) { + if (test = onFirstTest(suite, fn)) { return test; } + } +}; + +const getAllSiblingTests = function(suite, getTestById) { + const tests = []; + suite.eachTest(test => { + //# iterate through each of our suites tests. + //# this will iterate through all nested tests + //# as well. and then we add it only if its + //# in our grepp'd tests array + if (getTestById(test.id)) { + return tests.push(test); + } + }); + + return tests; +}; + +const getTestFromHook = function(hook, suite, getTestById) { + //# if theres already a currentTest use that + let found, test; + if (test = hook != null ? hook.ctx.currentTest : undefined) { return test; } + + //# if we have a hook id then attempt + //# to find the test by its id + if (hook != null ? hook.id : undefined) { + found = onFirstTest(suite, test => { + return hook.id === test.id; + }); + + if (found) { return found; } + } + + //# returns us the very first test + //# which is in our grepped tests array + //# based on walking down the current suite + //# iterating through each test until it matches + found = onFirstTest(suite, test => { + return getTestById(test.id); + }); + + if (found) { return found; } + + //# have one last final fallback where + //# we just return true on the very first + //# test (used in testing) + return onFirstTest(suite, test => true); +}; + +//# we have to see if this is the last suite amongst +//# its siblings. but first we have to filter out +//# suites which dont have a grep'd test in them +const isLastSuite = function(suite, tests) { + if (suite.root) { return false; } + + //# grab all of the suites from our grep'd tests + //# including all of their ancestor suites! + const suites = _.reduce(tests, function(memo, test) { + let parent; + while ((parent = test.parent)) { + memo.push(parent); + test = parent; + } + return memo; + } + , []); -wrapErr = (err) -> - reduceProps(err, ERROR_PROPS) - -getHookName = (hook) -> - ## find the name of the hook by parsing its - ## title and pulling out whats between the quotes - name = hook.title.match(betweenQuotesRe) - name and name[1] - -forceGc = (obj) -> - ## aggressively forces GC by purging - ## references to ctx, and removes callback - ## functions for closures - for own key, value of obj.ctx - obj.ctx[key] = undefined - - if obj.fn - obj.fn = -> - -anyTestInSuite = (suite, fn) -> - for test in suite.tests - return true if fn(test) is true - - for suite in suite.suites - return true if anyTestInSuite(suite, fn) is true - - ## else return false - return false - -eachHookInSuite = (suite, fn) -> - for type in HOOKS - for hook in suite["_" + type] - fn(hook) - - ## prevent loop comprehension - return null - -onFirstTest = (suite, fn) -> - for test in suite.tests - return test if fn(test) - - for suite in suite.suites - return test if test = onFirstTest(suite, fn) - -getAllSiblingTests = (suite, getTestById) -> - tests = [] - suite.eachTest (test) => - ## iterate through each of our suites tests. - ## this will iterate through all nested tests - ## as well. and then we add it only if its - ## in our grepp'd tests array - if getTestById(test.id) - tests.push test - - tests - -getTestFromHook = (hook, suite, getTestById) -> - ## if theres already a currentTest use that - return test if test = hook?.ctx.currentTest - - ## if we have a hook id then attempt - ## to find the test by its id - if hook?.id - found = onFirstTest suite, (test) => - hook.id is test.id - - return found if found - - ## returns us the very first test - ## which is in our grepped tests array - ## based on walking down the current suite - ## iterating through each test until it matches - found = onFirstTest suite, (test) => - getTestById(test.id) - - return found if found - - ## have one last final fallback where - ## we just return true on the very first - ## test (used in testing) - onFirstTest suite, (test) -> true - -## we have to see if this is the last suite amongst -## its siblings. but first we have to filter out -## suites which dont have a grep'd test in them -isLastSuite = (suite, tests) -> - return false if suite.root - - ## grab all of the suites from our grep'd tests - ## including all of their ancestor suites! - suites = _.reduce tests, (memo, test) -> - while parent = test.parent - memo.push(parent) - test = parent - memo - , [] - - ## intersect them with our parent suites and see if the last one is us - _ + //# intersect them with our parent suites and see if the last one is us + return _ .chain(suites) .uniq() .intersection(suite.parent.suites) .last() - .value() is suite - -## we are the last test that will run in the suite -## if we're the last test in the tests array or -## if we failed from a hook and that hook was 'before' -## since then mocha skips the remaining tests in the suite -lastTestThatWillRunInSuite = (test, tests) -> - isLastTest(test, tests) or (test.failedFromHookId and test.hookName is "before all") - -isLastTest = (test, tests) -> - test is _.last(tests) - -isRootSuite = (suite) -> - suite and suite.root - -overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, getTests) -> - ## bail if our _runner doesnt have a hook. - ## useful in tests - return if not _runner.hook - - ## monkey patch the hook event so we can wrap - ## 'test:after:run' around all of - ## the hooks surrounding a test runnable - _runnerHook = _runner.hook - - _runner.hook = (name, fn) -> - hooks = @suite["_" + name] - - allTests = getTests() - - changeFnToRunAfterHooks = -> - originalFn = fn - - test = getTest() - - ## reset fn to invoke the hooks - ## first but before calling next(err) - ## we fire our events - fn = -> - setTest(null) - - testAfterRun(test, Cypress) - - ## and now invoke next(err) - originalFn.apply(null, arguments) - - switch name - when "afterEach" - t = getTest() - - ## find all of the grep'd _tests which share - ## the same parent suite as our current _test - tests = getAllSiblingTests(t.parent, getTestById) - - ## make sure this test isnt the last test overall but also - ## isnt the last test in our grep'd parent suite's tests array - if @suite.root and (t isnt _.last(allTests)) and (t isnt _.last(tests)) - changeFnToRunAfterHooks() - - when "afterAll" - ## find all of the grep'd allTests which share - ## the same parent suite as our current _test - if t = getTest() - siblings = getAllSiblingTests(t.parent, getTestById) - - ## 1. if we're the very last test in the entire allTests - ## we wait until the root suite fires - ## 2. else we wait until the very last possible moment by waiting - ## until the root suite is the parent of the current suite - ## since that will bubble up IF we're the last nested suite - ## 3. else if we arent the last nested suite we fire if we're - ## the last test that will run + .value() === suite; +}; + +//# we are the last test that will run in the suite +//# if we're the last test in the tests array or +//# if we failed from a hook and that hook was 'before' +//# since then mocha skips the remaining tests in the suite +const lastTestThatWillRunInSuite = (test, tests) => isLastTest(test, tests) || (test.failedFromHookId && (test.hookName === "before all")); + +var isLastTest = (test, tests) => test === _.last(tests); + +const isRootSuite = suite => suite && suite.root; + +const overrideRunnerHook = function(Cypress, _runner, getTestById, getTest, setTest, getTests) { + //# bail if our _runner doesnt have a hook. + //# useful in tests + if (!_runner.hook) { return; } + + //# monkey patch the hook event so we can wrap + //# 'test:after:run' around all of + //# the hooks surrounding a test runnable + const _runnerHook = _runner.hook; + + return _runner.hook = function(name, fn) { + const hooks = this.suite[`_${name}`]; + + const allTests = getTests(); + + const changeFnToRunAfterHooks = function() { + const originalFn = fn; + + const test = getTest(); + + //# reset fn to invoke the hooks + //# first but before calling next(err) + //# we fire our events + return fn = function() { + setTest(null); + + testAfterRun(test, Cypress); + + //# and now invoke next(err) + return originalFn.apply(null, arguments); + }; + }; + + switch (name) { + case "afterEach": + var t = getTest(); + + //# find all of the grep'd _tests which share + //# the same parent suite as our current _test + var tests = getAllSiblingTests(t.parent, getTestById); + + //# make sure this test isnt the last test overall but also + //# isnt the last test in our grep'd parent suite's tests array + if (this.suite.root && (t !== _.last(allTests)) && (t !== _.last(tests))) { + changeFnToRunAfterHooks(); + } + break; + + case "afterAll": + //# find all of the grep'd allTests which share + //# the same parent suite as our current _test + if (t = getTest()) { + const siblings = getAllSiblingTests(t.parent, getTestById); + + //# 1. if we're the very last test in the entire allTests + //# we wait until the root suite fires + //# 2. else we wait until the very last possible moment by waiting + //# until the root suite is the parent of the current suite + //# since that will bubble up IF we're the last nested suite + //# 3. else if we arent the last nested suite we fire if we're + //# the last test that will run if ( - (isRootSuite(@suite) and isLastTest(t, allTests)) or - (isRootSuite(@suite.parent) and lastTestThatWillRunInSuite(t, siblings)) or - (not isLastSuite(@suite, allTests) and lastTestThatWillRunInSuite(t, siblings)) - ) - changeFnToRunAfterHooks() - - _runnerHook.call(@, name, fn) - -matchesGrep = (runnable, grep) -> - ## we have optimized this iteration to the maximum. - ## we memoize the existential matchesGrep property - ## so we dont regex again needlessly when going - ## through tests which have already been set earlier - if (not runnable.matchesGrep?) or (not _.isEqual(runnable.grepRe, grep)) - runnable.grepRe = grep - runnable.matchesGrep = grep.test(runnable.fullTitle()) - - runnable.matchesGrep - -getTestResults = (tests) -> - _.map tests, (test) -> - obj = _.pick(test, "id", "duration", "state") - obj.title = test.originalTitle - ## TODO FIX THIS! - if not obj.state - obj.state = "skipped" - obj - -normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnable, onLogsById, getTestId) -> - hasTests = false - - ## only loop until we find the first test - onFirstTest suite, (test) -> - hasTests = true - - ## if we dont have any tests then bail - return if not hasTests - - ## we are doing a super perf loop here where - ## we hand back a normalized object but also - ## create optimized lookups for the tests without - ## traversing through it multiple times - tests = {} - grepIsDefault = _.isEqual(grep, defaultGrepRe) - - obj = normalize(suite, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) - - if setTestsById - ## use callback here to hand back - ## the optimized tests - setTestsById(tests) - - if setTests - ## same pattern here - setTests(_.values(tests)) - - return obj - -normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) -> - normalizer = (runnable) => - runnable.id = getTestId() - - ## tests have a type of 'test' whereas suites do not have a type property - runnable.type ?= "suite" - - if onRunnable - onRunnable(runnable) - - ## if we have a runnable in the initial state - ## then merge in existing properties into the runnable - if i = initialTests[runnable.id] - _.each RUNNABLE_LOGS, (type) => - _.each i[type], onLogsById - - _.extend(runnable, i) - - ## reduce this runnable down to its props - ## and collections - return wrapAll(runnable) - - push = (test) => - tests[test.id] ?= test - - obj = normalizer(runnable) - - ## if we have a default grep then avoid - ## grepping altogether and just push - ## tests into the array of tests - if grepIsDefault - if runnable.type is "test" - push(runnable) - - ## and recursively iterate and normalize all other _runnables - _.each {tests: runnable.tests, suites: runnable.suites}, (_runnables, key) => - if runnable[key] - obj[key] = _.map _runnables, (runnable) => - normalize(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) - else - ## iterate through all tests and only push them in - ## if they match the current grep - obj.tests = _.reduce runnable.tests ? [], (memo, test) => - ## only push in the test if it matches - ## our grep - if matchesGrep(test, grep) - memo.push(normalizer(test)) - push(test) - memo - , [] - - ## and go through the suites - obj.suites = _.reduce runnable.suites ? [], (memo, suite) => - ## but only add them if a single nested test - ## actually matches the grep - any = anyTestInSuite suite, (test) => - matchesGrep(test, grep) - - if any + (isRootSuite(this.suite) && isLastTest(t, allTests)) || + (isRootSuite(this.suite.parent) && lastTestThatWillRunInSuite(t, siblings)) || + (!isLastSuite(this.suite, allTests) && lastTestThatWillRunInSuite(t, siblings)) + ) { + changeFnToRunAfterHooks(); + } + } + break; + } + + return _runnerHook.call(this, name, fn); + }; +}; + +const matchesGrep = function(runnable, grep) { + //# we have optimized this iteration to the maximum. + //# we memoize the existential matchesGrep property + //# so we dont regex again needlessly when going + //# through tests which have already been set earlier + if (((runnable.matchesGrep == null)) || (!_.isEqual(runnable.grepRe, grep))) { + runnable.grepRe = grep; + runnable.matchesGrep = grep.test(runnable.fullTitle()); + } + + return runnable.matchesGrep; +}; + +const getTestResults = tests => + _.map(tests, function(test) { + const obj = _.pick(test, "id", "duration", "state"); + obj.title = test.originalTitle; + //# TODO FIX THIS! + if (!obj.state) { + obj.state = "skipped"; + } + return obj; + }) +; + +const normalizeAll = function(suite, initialTests = {}, grep, setTestsById, setTests, onRunnable, onLogsById, getTestId) { + let hasTests = false; + + //# only loop until we find the first test + onFirstTest(suite, test => hasTests = true); + + //# if we dont have any tests then bail + if (!hasTests) { return; } + + //# we are doing a super perf loop here where + //# we hand back a normalized object but also + //# create optimized lookups for the tests without + //# traversing through it multiple times + const tests = {}; + const grepIsDefault = _.isEqual(grep, defaultGrepRe); + + const obj = normalize(suite, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId); + + if (setTestsById) { + //# use callback here to hand back + //# the optimized tests + setTestsById(tests); + } + + if (setTests) { + //# same pattern here + setTests(_.values(tests)); + } + + return obj; +}; + +var normalize = function(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) { + const normalizer = runnable => { + let i; + runnable.id = getTestId(); + + //# tests have a type of 'test' whereas suites do not have a type property + if (runnable.type == null) { runnable.type = "suite"; } + + if (onRunnable) { + onRunnable(runnable); + } + + //# if we have a runnable in the initial state + //# then merge in existing properties into the runnable + if (i = initialTests[runnable.id]) { + _.each(RUNNABLE_LOGS, type => { + return _.each(i[type], onLogsById); + }); + + _.extend(runnable, i); + } + + //# reduce this runnable down to its props + //# and collections + return wrapAll(runnable); + }; + + const push = test => { + return tests[test.id] != null ? tests[test.id] : (tests[test.id] = test); + }; + + const obj = normalizer(runnable); + + //# if we have a default grep then avoid + //# grepping altogether and just push + //# tests into the array of tests + if (grepIsDefault) { + if (runnable.type === "test") { + push(runnable); + } + + //# and recursively iterate and normalize all other _runnables + _.each({tests: runnable.tests, suites: runnable.suites}, (_runnables, key) => { + if (runnable[key]) { + return obj[key] = _.map(_runnables, runnable => { + return normalize(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId); + }); + } + }); + } else { + //# iterate through all tests and only push them in + //# if they match the current grep + obj.tests = _.reduce(runnable.tests != null ? runnable.tests : [], (memo, test) => { + //# only push in the test if it matches + //# our grep + if (matchesGrep(test, grep)) { + memo.push(normalizer(test)); + push(test); + } + return memo; + } + , []); + + //# and go through the suites + obj.suites = _.reduce(runnable.suites != null ? runnable.suites : [], (memo, suite) => { + //# but only add them if a single nested test + //# actually matches the grep + const any = anyTestInSuite(suite, test => { + return matchesGrep(test, grep); + }); + + if (any) { memo.push( normalize( suite, @@ -447,202 +524,228 @@ normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onL onLogsById, getTestId ) - ) - - memo - , [] - - return obj - -afterEachFailed = (Cypress, test, err) -> - test.state = "failed" - test.err = wrapErr(err) - - Cypress.action("runner:test:end", wrap(test)) - -hookFailed = (hook, err, hookName, getTestById, getTest) -> - ## finds the test by returning the first test from - ## the parent or looping through the suites until - ## it finds the first test - test = getTest() or getTestFromHook(hook, hook.parent, getTestById) - test.err = err - test.state = "failed" - test.duration = hook.duration ## TODO: nope (?) - test.hookName = hookName ## TODO: why are we doing this? - test.failedFromHookId = hook.hookId - - if hook.alreadyEmittedMocha - ## TODO: won't this always hit right here??? - ## when would the hook not be emitted at this point? - test.alreadyEmittedMocha = true - else - Cypress.action("runner:test:end", wrap(test)) - -_runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) -> - _runner.on "start", -> + ); + } + + return memo; + } + , []); + } + + return obj; +}; + +const afterEachFailed = function(Cypress, test, err) { + test.state = "failed"; + test.err = wrapErr(err); + + return Cypress.action("runner:test:end", wrap(test)); +}; + +const hookFailed = function(hook, err, hookName, getTestById, getTest) { + //# finds the test by returning the first test from + //# the parent or looping through the suites until + //# it finds the first test + const test = getTest() || getTestFromHook(hook, hook.parent, getTestById); + test.err = err; + test.state = "failed"; + test.duration = hook.duration; //# TODO: nope (?) + test.hookName = hookName; //# TODO: why are we doing this? + test.failedFromHookId = hook.hookId; + + if (hook.alreadyEmittedMocha) { + //# TODO: won't this always hit right here??? + //# when would the hook not be emitted at this point? + return test.alreadyEmittedMocha = true; + } else { + return Cypress.action("runner:test:end", wrap(test)); + } +}; + +const _runnerListeners = function(_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) { + _runner.on("start", () => Cypress.action("runner:start", { start: new Date() }) + ); - _runner.on "end", -> + _runner.on("end", () => Cypress.action("runner:end", { end: new Date() }) + ); - _runner.on "suite", (suite) -> - return if _emissions.started[suite.id] + _runner.on("suite", function(suite) { + if (_emissions.started[suite.id]) { return; } - _emissions.started[suite.id] = true + _emissions.started[suite.id] = true; - Cypress.action("runner:suite:start", wrap(suite)) + return Cypress.action("runner:suite:start", wrap(suite)); + }); - _runner.on "suite end", (suite) -> - ## cleanup our suite + its hooks - forceGc(suite) - eachHookInSuite(suite, forceGc) + _runner.on("suite end", function(suite) { + //# cleanup our suite + its hooks + forceGc(suite); + eachHookInSuite(suite, forceGc); - return if _emissions.ended[suite.id] + if (_emissions.ended[suite.id]) { return; } - _emissions.ended[suite.id] = true + _emissions.ended[suite.id] = true; - Cypress.action("runner:suite:end", wrap(suite)) + return Cypress.action("runner:suite:end", wrap(suite)); + }); - _runner.on "hook", (hook) -> - hook.hookId ?= getHookId() - hook.hookName ?= getHookName(hook) + _runner.on("hook", function(hook) { + if (hook.hookId == null) { hook.hookId = getHookId(); } + if (hook.hookName == null) { hook.hookName = getHookName(hook); } - ## mocha incorrectly sets currentTest on before all's. - ## if there is a nested suite with a before, then - ## currentTest will refer to the previous test run - ## and not our current - if hook.hookName is "before all" and hook.ctx.currentTest - delete hook.ctx.currentTest + //# mocha incorrectly sets currentTest on before all's. + //# if there is a nested suite with a before, then + //# currentTest will refer to the previous test run + //# and not our current + if ((hook.hookName === "before all") && hook.ctx.currentTest) { + delete hook.ctx.currentTest; + } - ## set the hook's id from the test because - ## hooks do not have their own id, their - ## commands need to grouped with the test - ## and we can only associate them by this id - test = getTest() or getTestFromHook(hook, hook.parent, getTestById) - hook.id = test.id - hook.ctx.currentTest = test + //# set the hook's id from the test because + //# hooks do not have their own id, their + //# commands need to grouped with the test + //# and we can only associate them by this id + const test = getTest() || getTestFromHook(hook, hook.parent, getTestById); + hook.id = test.id; + hook.ctx.currentTest = test; - ## make sure we set this test as the current now - ## else its possible that our TEST_AFTER_RUN_EVENT - ## will never fire if this failed in a before hook - setTest(test) + //# make sure we set this test as the current now + //# else its possible that our TEST_AFTER_RUN_EVENT + //# will never fire if this failed in a before hook + setTest(test); - Cypress.action("runner:hook:start", wrap(hook)) + return Cypress.action("runner:hook:start", wrap(hook)); + }); - _runner.on "hook end", (hook) -> - Cypress.action("runner:hook:end", wrap(hook)) + _runner.on("hook end", hook => Cypress.action("runner:hook:end", wrap(hook))); - _runner.on "test", (test) -> - setTest(test) + _runner.on("test", function(test) { + setTest(test); - return if _emissions.started[test.id] + if (_emissions.started[test.id]) { return; } - _emissions.started[test.id] = true + _emissions.started[test.id] = true; - Cypress.action("runner:test:start", wrap(test)) + return Cypress.action("runner:test:start", wrap(test)); + }); - _runner.on "test end", (test) -> - return if _emissions.ended[test.id] + _runner.on("test end", function(test) { + if (_emissions.ended[test.id]) { return; } - _emissions.ended[test.id] = true + _emissions.ended[test.id] = true; - Cypress.action("runner:test:end", wrap(test)) + return Cypress.action("runner:test:end", wrap(test)); + }); - _runner.on "pass", (test) -> - Cypress.action("runner:pass", wrap(test)) + _runner.on("pass", test => Cypress.action("runner:pass", wrap(test))); - ## if a test is pending mocha will only - ## emit the pending event instead of the test - ## so we normalize the pending / test events - _runner.on "pending", (test) -> - ## do nothing if our test is skipped - return if test._ALREADY_RAN + //# if a test is pending mocha will only + //# emit the pending event instead of the test + //# so we normalize the pending / test events + _runner.on("pending", function(test) { + //# do nothing if our test is skipped + if (test._ALREADY_RAN) { return; } - if not fired(TEST_BEFORE_RUN_EVENT, test) - fire(TEST_BEFORE_RUN_EVENT, test, Cypress) + if (!fired(TEST_BEFORE_RUN_EVENT, test)) { + fire(TEST_BEFORE_RUN_EVENT, test, Cypress); + } - test.state = "pending" + test.state = "pending"; - if not test.alreadyEmittedMocha - ## do not double emit this event - test.alreadyEmittedMocha = true + if (!test.alreadyEmittedMocha) { + //# do not double emit this event + test.alreadyEmittedMocha = true; - Cypress.action("runner:pending", wrap(test)) + Cypress.action("runner:pending", wrap(test)); + } - @emit("test", test) + this.emit("test", test); - ## if this is not the last test amongst its siblings - ## then go ahead and fire its test:after:run event - ## else this will not get called - tests = getAllSiblingTests(test.parent, getTestById) + //# if this is not the last test amongst its siblings + //# then go ahead and fire its test:after:run event + //# else this will not get called + const tests = getAllSiblingTests(test.parent, getTestById); - if _.last(tests) isnt test - fire(TEST_AFTER_RUN_EVENT, test, Cypress) + if (_.last(tests) !== test) { + return fire(TEST_AFTER_RUN_EVENT, test, Cypress); + } + }); - _runner.on "fail", (runnable, err) -> - isHook = runnable.type is "hook" + return _runner.on("fail", function(runnable, err) { + let hookName; + const isHook = runnable.type === "hook"; - if isHook - parentTitle = runnable.parent.title - hookName = getHookName(runnable) + if (isHook) { + const parentTitle = runnable.parent.title; + hookName = getHookName(runnable); - ## append a friendly message to the error indicating - ## we're skipping the remaining tests in this suite + //# append a friendly message to the error indicating + //# we're skipping the remaining tests in this suite err = $utils.appendErrMsg( err, $utils.errMessageByPath("uncaught.error_in_hook", { parentTitle, hookName }) - ) - - ## always set runnable err so we can tap into - ## taking a screenshot on error - runnable.err = wrapErr(err) - - if not runnable.alreadyEmittedMocha - ## do not double emit this event - runnable.alreadyEmittedMocha = true - - Cypress.action("runner:fail", wrap(runnable), runnable.err) - - ## if we've already fired the test after run event - ## it means that this runnable likely failed due to - ## a double done(err) callback, and we need to fire - ## this again! - if fired(TEST_AFTER_RUN_EVENT, runnable) - fire(TEST_AFTER_RUN_EVENT, runnable, Cypress) - - if isHook - ## if a hook fails (such as a before) then the test will never - ## get run and we'll need to make sure we set the test so that - ## the TEST_AFTER_RUN_EVENT fires correctly - hookFailed(runnable, runnable.err, hookName, getTestById, getTest) - -create = (specWindow, mocha, Cypress, cy) -> - _id = 0 - _hookId = 0 - _uncaughtFn = null - - _runner = mocha.getRunner() - _runner.suite = mocha.getRootSuite() - - specWindow.onerror = -> - err = cy.onSpecWindowUncaughtException.apply(cy, arguments) - - ## err will not be returned if cy can associate this - ## uncaught exception to an existing runnable - return true if not err - - todoMsg = -> - if not Cypress.config("isTextTerminal") - "Check your console for the stack trace or click this message to see where it originated from." - - append = -> + ); + } + + //# always set runnable err so we can tap into + //# taking a screenshot on error + runnable.err = wrapErr(err); + + if (!runnable.alreadyEmittedMocha) { + //# do not double emit this event + runnable.alreadyEmittedMocha = true; + + Cypress.action("runner:fail", wrap(runnable), runnable.err); + } + + //# if we've already fired the test after run event + //# it means that this runnable likely failed due to + //# a double done(err) callback, and we need to fire + //# this again! + if (fired(TEST_AFTER_RUN_EVENT, runnable)) { + fire(TEST_AFTER_RUN_EVENT, runnable, Cypress); + } + + if (isHook) { + //# if a hook fails (such as a before) then the test will never + //# get run and we'll need to make sure we set the test so that + //# the TEST_AFTER_RUN_EVENT fires correctly + return hookFailed(runnable, runnable.err, hookName, getTestById, getTest); + } + }); +}; + +const create = function(specWindow, mocha, Cypress, cy) { + let _id = 0; + let _hookId = 0; + let _uncaughtFn = null; + + const _runner = mocha.getRunner(); + _runner.suite = mocha.getRootSuite(); + + specWindow.onerror = function() { + let err = cy.onSpecWindowUncaughtException.apply(cy, arguments); + + //# err will not be returned if cy can associate this + //# uncaught exception to an existing runnable + if (!err) { return true; } + + const todoMsg = function() { + if (!Cypress.config("isTextTerminal")) { + return "Check your console for the stack trace or click this message to see where it originated from."; + } + }; + + const append = () => _.chain([ "Cypress could not associate this error to any specific test.", "We dynamically generated a new test to display this failure.", @@ -650,443 +753,499 @@ create = (specWindow, mocha, Cypress, cy) -> ]) .compact() .join("\n\n") - - ## else do the same thing as mocha here - err = $utils.appendErrMsg(err, append()) - - throwErr = -> - throw err - - ## we could not associate this error - ## and shouldn't ever start our run - _uncaughtFn = throwErr - - ## return undefined so the browser does its default - ## uncaught exception behavior (logging to console) - return undefined - - ## hold onto the _runnables for faster lookup later - _stopped = false - _test = null - _tests = [] - _testsById = {} - _testsQueue = [] - _testsQueueById = {} - _runnables = [] - _logsById = {} - _emissions = { - started: {} + ; + + //# else do the same thing as mocha here + err = $utils.appendErrMsg(err, append()); + + const throwErr = function() { + throw err; + }; + + //# we could not associate this error + //# and shouldn't ever start our run + _uncaughtFn = throwErr; + + //# return undefined so the browser does its default + //# uncaught exception behavior (logging to console) + return undefined; + }; + + //# hold onto the _runnables for faster lookup later + let _stopped = false; + let _test = null; + let _tests = []; + let _testsById = {}; + const _testsQueue = []; + const _testsQueueById = {}; + const _runnables = []; + const _logsById = {}; + let _emissions = { + started: {}, ended: {} - } - _startTime = null + }; + let _startTime = null; - getTestId = -> - ## increment the id counter - "r" + (_id += 1) + const getTestId = () => + //# increment the id counter + `r${_id += 1}` + ; - getHookId = -> - "h" + (_hookId += 1) + const getHookId = () => `h${_hookId += 1}`; - setTestsById = (tbid) -> - _testsById = tbid + const setTestsById = tbid => _testsById = tbid; - setTests = (t) -> - _tests = t + const setTests = t => _tests = t; - getTests = -> - _tests + const getTests = () => _tests; - onRunnable = (r) -> - _runnables.push(r) + const onRunnable = r => _runnables.push(r); - onLogsById = (l) -> - _logsById[l.id] = l + const onLogsById = l => _logsById[l.id] = l; - getTest = -> - _test + const getTest = () => _test; - setTest = (t) -> - _test = t + const setTest = t => _test = t; - getTestById = (id) -> - ## perf short circuit - return if not id + const getTestById = function(id) { + //# perf short circuit + if (!id) { return; } - _testsById[id] + return _testsById[id]; + }; - overrideRunnerHook(Cypress, _runner, getTestById, getTest, setTest, getTests) + overrideRunnerHook(Cypress, _runner, getTestById, getTest, setTest, getTests); return { - grep: (re) -> - if arguments.length - _runner._grep = re - else - ## grab grep from the mocha _runner - ## or just set it to all in case - ## there is a mocha regression - _runner._grep ?= defaultGrepRe - - options: (options = {}) -> - ## TODO - ## need to handle - ## ignoreLeaks, asyncOnly, globals - - if re = options.grep - @grep(re) - - normalizeAll: (tests) -> - ## if we have an uncaught error then slice out - ## all of the tests and suites and just generate - ## a single test since we received an uncaught - ## error prior to processing any of mocha's tests - ## which could have occurred in a separate support file - if _uncaughtFn - _runner.suite.suites = [] - _runner.suite.tests = [] - - ## create a runnable to associate for the failure - mocha.createRootTest("An uncaught error was detected outside of a test", _uncaughtFn) - - normalizeAll( + grep(re) { + if (arguments.length) { + return _runner._grep = re; + } else { + //# grab grep from the mocha _runner + //# or just set it to all in case + //# there is a mocha regression + return _runner._grep != null ? _runner._grep : (_runner._grep = defaultGrepRe); + } + }, + + options(options = {}) { + //# TODO + //# need to handle + //# ignoreLeaks, asyncOnly, globals + + let re; + if (re = options.grep) { + return this.grep(re); + } + }, + + normalizeAll(tests) { + //# if we have an uncaught error then slice out + //# all of the tests and suites and just generate + //# a single test since we received an uncaught + //# error prior to processing any of mocha's tests + //# which could have occurred in a separate support file + if (_uncaughtFn) { + _runner.suite.suites = []; + _runner.suite.tests = []; + + //# create a runnable to associate for the failure + mocha.createRootTest("An uncaught error was detected outside of a test", _uncaughtFn); + } + + return normalizeAll( _runner.suite, tests, - @grep(), + this.grep(), setTestsById, setTests, onRunnable, onLogsById, getTestId - ) - - run: (fn) -> - _startTime ?= moment().toJSON() - - _runnerListeners(_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) - - _runner.run (failures) -> - ## if we happen to make it all the way through - ## the run, then just set _stopped to true here - _stopped = true - - ## TODO this functions is not correctly - ## synchronized with the 'end' event that - ## we manage because of uncaught hook errors - if fn - fn(failures, getTestResults(_tests)) - - onRunnableRun: (runnableRun, runnable, args) -> - if not runnable.id - throw new Error("runnable must have an id", runnable.id) - - switch runnable.type - when "hook" - test = getTest() or getTestFromHook(runnable, runnable.parent, getTestById) - - when "test" - test = runnable - - ## closure for calculating the actual - ## runtime of a runnables fn exection duration - ## and also the run of the runnable:after:run:async event - wallClockStartedAt = null - wallClockEnd = null - fnDurationStart = null - fnDurationEnd = null - afterFnDurationStart = null - afterFnDurationEnd = null - - ## when this is a hook, capture the real start - ## date so we can calculate our test's duration - ## including all of its hooks - wallClockStartedAt = new Date() - - if not test.wallClockStartedAt - ## if we don't have lifecycle timings yet - lifecycleStart = wallClockStartedAt - - test.wallClockStartedAt ?= wallClockStartedAt - - ## if this isnt a hook, then the name is 'test' - hookName = if runnable.type is "hook" then getHookName(runnable) else "test" - - ## if we haven't yet fired this event for this test - ## that means that we need to reset the previous state - ## of cy - since we now have a new 'test' and all of the - ## associated _runnables will share this state - if not fired(TEST_BEFORE_RUN_EVENT, test) - fire(TEST_BEFORE_RUN_EVENT, test, Cypress) - - ## extract out the next(fn) which mocha uses to - ## move to the next runnable - this will be our async seam - _next = args[0] - - next = (err) -> - ## now set the duration of the after runnable run async event - afterFnDurationEnd = wallClockEnd = new Date() - - switch runnable.type - when "hook" - ## reset runnable duration to include lifecycle - ## and afterFn timings purely for the mocha runner. - ## this is what it 'feels' like to the user - runnable.duration = wallClockEnd - wallClockStartedAt + ); + }, + + run(fn) { + if (_startTime == null) { _startTime = moment().toJSON(); } + + _runnerListeners(_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId); + + return _runner.run(function(failures) { + //# if we happen to make it all the way through + //# the run, then just set _stopped to true here + _stopped = true; + + //# TODO this functions is not correctly + //# synchronized with the 'end' event that + //# we manage because of uncaught hook errors + if (fn) { + return fn(failures, getTestResults(_tests)); + } + }); + }, + + onRunnableRun(runnableRun, runnable, args) { + let lifecycleStart, test; + if (!runnable.id) { + throw new Error("runnable must have an id", runnable.id); + } + + switch (runnable.type) { + case "hook": + test = getTest() || getTestFromHook(runnable, runnable.parent, getTestById); + break; + + case "test": + test = runnable; + break; + } + + //# closure for calculating the actual + //# runtime of a runnables fn exection duration + //# and also the run of the runnable:after:run:async event + let wallClockStartedAt = null; + let wallClockEnd = null; + let fnDurationStart = null; + let fnDurationEnd = null; + let afterFnDurationStart = null; + let afterFnDurationEnd = null; + + //# when this is a hook, capture the real start + //# date so we can calculate our test's duration + //# including all of its hooks + wallClockStartedAt = new Date(); + + if (!test.wallClockStartedAt) { + //# if we don't have lifecycle timings yet + lifecycleStart = wallClockStartedAt; + } + + if (test.wallClockStartedAt == null) { test.wallClockStartedAt = wallClockStartedAt; } + + //# if this isnt a hook, then the name is 'test' + const hookName = runnable.type === "hook" ? getHookName(runnable) : "test"; + + //# if we haven't yet fired this event for this test + //# that means that we need to reset the previous state + //# of cy - since we now have a new 'test' and all of the + //# associated _runnables will share this state + if (!fired(TEST_BEFORE_RUN_EVENT, test)) { + fire(TEST_BEFORE_RUN_EVENT, test, Cypress); + } + + //# extract out the next(fn) which mocha uses to + //# move to the next runnable - this will be our async seam + const _next = args[0]; + + const next = function(err) { + //# now set the duration of the after runnable run async event + afterFnDurationEnd = (wallClockEnd = new Date()); + + switch (runnable.type) { + case "hook": + //# reset runnable duration to include lifecycle + //# and afterFn timings purely for the mocha runner. + //# this is what it 'feels' like to the user + runnable.duration = wallClockEnd - wallClockStartedAt; setTestTimingsForHook(test, hookName, { - hookId: runnable.hookId - fnDuration: fnDurationEnd - fnDurationStart + hookId: runnable.hookId, + fnDuration: fnDurationEnd - fnDurationStart, afterFnDuration: afterFnDurationEnd - afterFnDurationStart - }) + }); + break; - when "test" - ## if we are currently on a test then - ## recalculate its duration to be based - ## against that (purely for the mocha reporter) - test.duration = wallClockEnd - test.wallClockStartedAt + case "test": + //# if we are currently on a test then + //# recalculate its duration to be based + //# against that (purely for the mocha reporter) + test.duration = wallClockEnd - test.wallClockStartedAt; - ## but still preserve its actual function - ## body duration for timings + //# but still preserve its actual function + //# body duration for timings setTestTimings(test, "test", { - fnDuration: fnDurationEnd - fnDurationStart + fnDuration: fnDurationEnd - fnDurationStart, afterFnDuration: afterFnDurationEnd - afterFnDurationStart + }); + break; + } + + return _next(err); + }; + + const onNext = function(err) { + //# when done with the function set that to end + fnDurationEnd = new Date(); + + //# and also set the afterFnDuration to this same date + afterFnDurationStart = fnDurationEnd; + + //# attach error right now + //# if we have one + if (err) { + if (err instanceof Pending) { + err.isPending = true; + } + + runnable.err = wrapErr(err); + } + + return runnableAfterRunAsync(runnable, Cypress) + .then(function() { + //# once we complete callback with the + //# original err + next(err); + + //# return null here to signal to bluebird + //# that we did not forget to return a promise + //# because mocha internally does not return + //# the test.run(fn) + return null;}).catch(function(err) { + next(err); + + //# return null here to signal to bluebird + //# that we did not forget to return a promise + //# because mocha internally does not return + //# the test.run(fn) + return null; + }); + }; + + //# our runnable is about to run, so let cy know. this enables + //# us to always have a correct runnable set even when we are + //# running lifecycle events + //# and also get back a function result handler that we use as + //# an async seam + cy.setRunnable(runnable, hookName); + + //# TODO: handle promise timeouts here! + //# whenever any runnable is about to run + //# we figure out what test its associated to + //# if its a hook, and then we fire the + //# test:before:run:async action if its not + //# been fired before for this test + return testBeforeRunAsync(test, Cypress) + .catch(function(err) { + //# TODO: if our async tasks fail + //# then allow us to cause the test + //# to fail here by blowing up its fn + //# callback + const { fn } = runnable; + + const restore = () => runnable.fn = fn; + + return runnable.fn = function() { + restore(); + + throw err; + };}).finally(function() { + if (lifecycleStart) { + //# capture how long the lifecycle took as part + //# of the overall wallClockDuration of our test + setTestTimings(test, "lifecycle", new Date() - lifecycleStart); + } + + //# capture the moment we're about to invoke + //# the runnable's callback function + fnDurationStart = new Date(); + + //# call the original method with our + //# custom onNext function + return runnableRun.call(runnable, onNext); + }); + }, + + getStartTime() { + return _startTime; + }, + + setStartTime(iso) { + return _startTime = iso; + }, + + countByTestState(tests, state) { + const count = _.filter(tests, (test, key) => test.state === state); + + return count.length; + }, + + setNumLogs(num) { + return $Log.setCounter(num); + }, + + getEmissions() { + return _emissions; + }, + + getTestsState() { + const id = _test != null ? _test.id : undefined; + const tests = {}; + + //# bail if we dont have a current test + if (!id) { return {}; } + + //# search through all of the tests + //# until we find the current test + //# and break then + for (var test of _tests) { + if (test.id === id) { + break; + } else { + test = wrapAll(test); + + _.each(RUNNABLE_LOGS, function(type) { + let logs; + if (logs = test[type]) { + return test[type] = _.map(logs, $Log.toSerializedJSON); + } + }); + + tests[test.id] = test; + } + } + + return tests; + }, + + stop() { + if (_stopped) { + return; + } + + _stopped = true; + + //# abort the run + _runner.abort(); + + //# emit the final 'end' event + //# since our reporter depends on this event + //# and mocha may never fire this becuase our + //# runnable may never finish + _runner.emit("end"); + + //# remove all the listeners + //# so no more events fire + return _runner.removeAllListeners(); + }, + + getDisplayPropsForLog: $Log.getDisplayProps, + + getConsolePropsForLogById(logId) { + let attrs; + if (attrs = _logsById[logId]) { + return $Log.getConsoleProps(attrs); + } + }, + + getSnapshotPropsForLogById(logId) { + let attrs; + if (attrs = _logsById[logId]) { + return $Log.getSnapshotProps(attrs); + } + }, + + getErrorByTestId(testId) { + let test; + if (test = getTestById(testId)) { + return wrapErr(test.err); + } + }, + + resumeAtTest(id, emissions = {}) { + Cypress._RESUMED_AT_TEST = id; + + _emissions = emissions; + + for (let test of _tests) { + if (test.id !== id) { + test._ALREADY_RAN = true; + test.pending = true; + } else { + //# bail so we can stop now + return; + } + } + }, + + cleanupQueue(numTestsKeptInMemory) { + var cleanup = function(queue) { + if (queue.length > numTestsKeptInMemory) { + const test = queue.shift(); + + delete _testsQueueById[test.id]; + + _.each(RUNNABLE_LOGS, logs => + _.each(test[logs], function(attrs) { + //# we know our attrs have been cleaned + //# now, so lets store that + attrs._hasBeenCleanedUp = true; + + return $Log.reduceMemory(attrs); }) - - _next(err) - - onNext = (err) -> - ## when done with the function set that to end - fnDurationEnd = new Date() - - ## and also set the afterFnDuration to this same date - afterFnDurationStart = fnDurationEnd - - ## attach error right now - ## if we have one - if err - if err instanceof Pending - err.isPending = true - - runnable.err = wrapErr(err) - - runnableAfterRunAsync(runnable, Cypress) - .then -> - ## once we complete callback with the - ## original err - next(err) - - ## return null here to signal to bluebird - ## that we did not forget to return a promise - ## because mocha internally does not return - ## the test.run(fn) - return null - - ## if these promises fail then reset the - ## error to that - .catch (err) -> - next(err) - - ## return null here to signal to bluebird - ## that we did not forget to return a promise - ## because mocha internally does not return - ## the test.run(fn) - return null - - ## our runnable is about to run, so let cy know. this enables - ## us to always have a correct runnable set even when we are - ## running lifecycle events - ## and also get back a function result handler that we use as - ## an async seam - cy.setRunnable(runnable, hookName) - - ## TODO: handle promise timeouts here! - ## whenever any runnable is about to run - ## we figure out what test its associated to - ## if its a hook, and then we fire the - ## test:before:run:async action if its not - ## been fired before for this test - testBeforeRunAsync(test, Cypress) - .catch (err) -> - ## TODO: if our async tasks fail - ## then allow us to cause the test - ## to fail here by blowing up its fn - ## callback - fn = runnable.fn - - restore = -> - runnable.fn = fn - - runnable.fn = -> - restore() - - throw err - .finally -> - if lifecycleStart - ## capture how long the lifecycle took as part - ## of the overall wallClockDuration of our test - setTestTimings(test, "lifecycle", new Date() - lifecycleStart) - - ## capture the moment we're about to invoke - ## the runnable's callback function - fnDurationStart = new Date() - - ## call the original method with our - ## custom onNext function - runnableRun.call(runnable, onNext) - - getStartTime: -> - _startTime - - setStartTime: (iso) -> - _startTime = iso - - countByTestState: (tests, state) -> - count = _.filter tests, (test, key) -> - test.state is state - - count.length - - setNumLogs: (num) -> - $Log.setCounter(num) - - getEmissions: -> - _emissions - - getTestsState: -> - id = _test?.id - tests = {} - - ## bail if we dont have a current test - return {} if not id - - ## search through all of the tests - ## until we find the current test - ## and break then - for test in _tests - if test.id is id - break - else - test = wrapAll(test) - - _.each RUNNABLE_LOGS, (type) -> - if logs = test[type] - test[type] = _.map(logs, $Log.toSerializedJSON) - - tests[test.id] = test - - return tests - - stop: -> - if _stopped - return - - _stopped = true - - ## abort the run - _runner.abort() - - ## emit the final 'end' event - ## since our reporter depends on this event - ## and mocha may never fire this becuase our - ## runnable may never finish - _runner.emit("end") - - ## remove all the listeners - ## so no more events fire - _runner.removeAllListeners() - - getDisplayPropsForLog: $Log.getDisplayProps - - getConsolePropsForLogById: (logId) -> - if attrs = _logsById[logId] - $Log.getConsoleProps(attrs) - - getSnapshotPropsForLogById: (logId) -> - if attrs = _logsById[logId] - $Log.getSnapshotProps(attrs) - - getErrorByTestId: (testId) -> - if test = getTestById(testId) - wrapErr(test.err) - - resumeAtTest: (id, emissions = {}) -> - Cypress._RESUMED_AT_TEST = id - - _emissions = emissions - - for test in _tests - if test.id isnt id - test._ALREADY_RAN = true - test.pending = true - else - ## bail so we can stop now - return - - cleanupQueue: (numTestsKeptInMemory) -> - cleanup = (queue) -> - if queue.length > numTestsKeptInMemory - test = queue.shift() - - delete _testsQueueById[test.id] - - _.each RUNNABLE_LOGS, (logs) -> - _.each test[logs], (attrs) -> - ## we know our attrs have been cleaned - ## now, so lets store that - attrs._hasBeenCleanedUp = true - - $Log.reduceMemory(attrs) - - cleanup(queue) - - cleanup(_testsQueue) - - addLog: (attrs, isInteractive) -> - ## we dont need to hold a log reference - ## to anything in memory when we're headless - ## because you cannot inspect any logs - return if not isInteractive - - test = getTestById(attrs.testId) - - ## bail if for whatever reason we - ## cannot associate this log to a test - return if not test - - ## if this test isnt in the current queue - ## then go ahead and add it - if not _testsQueueById[test.id] - _testsQueueById[test.id] = true - _testsQueue.push(test) - - if existing = _logsById[attrs.id] - ## because log:state:changed may - ## fire at a later time, its possible - ## we've already cleaned up these attrs - ## and in that case we don't want to do - ## anything at all - return if existing._hasBeenCleanedUp - - ## mutate the existing object - _.extend(existing, attrs) - else - _logsById[attrs.id] = attrs - - { testId, instrument } = attrs - - if test = getTestById(testId) - ## pluralize the instrument - ## as a property on the runnable - logs = test[instrument + "s"] ?= [] - - ## else push it onto the logs - logs.push(attrs) - } + ); + + return cleanup(queue); + } + }; + + return cleanup(_testsQueue); + }, + + addLog(attrs, isInteractive) { + //# we dont need to hold a log reference + //# to anything in memory when we're headless + //# because you cannot inspect any logs + let existing; + if (!isInteractive) { return; } + + let test = getTestById(attrs.testId); + + //# bail if for whatever reason we + //# cannot associate this log to a test + if (!test) { return; } + + //# if this test isnt in the current queue + //# then go ahead and add it + if (!_testsQueueById[test.id]) { + _testsQueueById[test.id] = true; + _testsQueue.push(test); + } + + if (existing = _logsById[attrs.id]) { + //# because log:state:changed may + //# fire at a later time, its possible + //# we've already cleaned up these attrs + //# and in that case we don't want to do + //# anything at all + if (existing._hasBeenCleanedUp) { return; } + + //# mutate the existing object + return _.extend(existing, attrs); + } else { + _logsById[attrs.id] = attrs; + + const { testId, instrument } = attrs; + + if (test = getTestById(testId)) { + //# pluralize the instrument + //# as a property on the runnable + let name; + const logs = test[name = instrument + "s"] != null ? test[name] : (test[name] = []); + + //# else push it onto the logs + return logs.push(attrs); + } + } + } + }; +}; module.exports = { - overrideRunnerHook + overrideRunnerHook, - normalize + normalize, - normalizeAll + normalizeAll, create -} +}; From 599b00cd649b158fe29df472d9eece03c08b126c Mon Sep 17 00:00:00 2001 From: decaffeinate <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:29:45 -0500 Subject: [PATCH 004/300] decaffeinate: Run post-processing cleanups on runner.coffee --- packages/driver/src/cypress/runner.js | 1183 ++++++++++++++----------- 1 file changed, 666 insertions(+), 517 deletions(-) diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index b438a652218e..12978940a63a 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -1,3 +1,17 @@ +/* eslint-disable + brace-style, + default-case, + no-case-declarations, + no-cond-assign, + no-const-assign, + no-undef, + no-unused-vars, + no-var, + one-var, + prefer-rest-params, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -6,25 +20,25 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const _ = require("lodash"); -const moment = require("moment"); -const Promise = require("bluebird"); -const Pending = require("mocha/lib/pending"); +const _ = require('lodash') +const moment = require('moment') +const Promise = require('bluebird') +const Pending = require('mocha/lib/pending') -const $Log = require("./log"); -const $utils = require("./utils"); +const $Log = require('./log') +const $utils = require('./utils') -const defaultGrepRe = /.*/; -const mochaCtxKeysRe = /^(_runnable|test)$/; -const betweenQuotesRe = /\"(.+?)\"/; +const defaultGrepRe = /.*/ +const mochaCtxKeysRe = /^(_runnable|test)$/ +const betweenQuotesRe = /\"(.+?)\"/ -const HOOKS = "beforeAll beforeEach afterEach afterAll".split(" "); -const TEST_BEFORE_RUN_EVENT = "runner:test:before:run"; -const TEST_AFTER_RUN_EVENT = "runner:test:after:run"; +const HOOKS = 'beforeAll beforeEach afterEach afterAll'.split(' ') +const TEST_BEFORE_RUN_EVENT = 'runner:test:before:run' +const TEST_AFTER_RUN_EVENT = 'runner:test:after:run' -const ERROR_PROPS = "message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending".split(" "); -const RUNNABLE_LOGS = "routes agents commands".split(" "); -const RUNNABLE_PROPS = "id title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings".split(" "); +const ERROR_PROPS = 'message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff isPending'.split(' ') +const RUNNABLE_LOGS = 'routes agents commands'.split(' ') +const RUNNABLE_PROPS = 'id title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings'.split(' ') // ## initial payload // { @@ -72,222 +86,269 @@ const RUNNABLE_PROPS = "id title root hookName hookId err state failedFromHook // ] // } -const fire = function(event, runnable, Cypress) { - if (runnable._fired == null) { runnable._fired = {}; } - runnable._fired[event] = true; +const fire = function (event, runnable, Cypress) { + if (runnable._fired == null) { + runnable._fired = {} + } + + runnable._fired[event] = true //# dont fire anything again if we are skipped - if (runnable._ALREADY_RAN) { return; } + if (runnable._ALREADY_RAN) { + return + } - return Cypress.action(event, wrap(runnable), runnable); -}; + return Cypress.action(event, wrap(runnable), runnable) +} -const fired = (event, runnable) => !!(runnable._fired && runnable._fired[event]); +const fired = (event, runnable) => { + return !!(runnable._fired && runnable._fired[event]) +} -const testBeforeRunAsync = (test, Cypress) => - Promise.try(function() { - if (!fired("runner:test:before:run:async", test)) { - return fire("runner:test:before:run:async", test, Cypress); +const testBeforeRunAsync = (test, Cypress) => { + return Promise.try(() => { + if (!fired('runner:test:before:run:async', test)) { + return fire('runner:test:before:run:async', test, Cypress) } }) -; +} -const runnableAfterRunAsync = (runnable, Cypress) => - Promise.try(function() { - if (!fired("runner:runnable:after:run:async", runnable)) { - return fire("runner:runnable:after:run:async", runnable, Cypress); +const runnableAfterRunAsync = (runnable, Cypress) => { + return Promise.try(() => { + if (!fired('runner:runnable:after:run:async', runnable)) { + return fire('runner:runnable:after:run:async', runnable, Cypress) } }) -; +} -const testAfterRun = function(test, Cypress) { +const testAfterRun = function (test, Cypress) { if (!fired(TEST_AFTER_RUN_EVENT, test)) { - setWallClockDuration(test); - fire(TEST_AFTER_RUN_EVENT, test, Cypress); + setWallClockDuration(test) + fire(TEST_AFTER_RUN_EVENT, test, Cypress) //# perf loop only through //# a tests OWN properties and not //# inherited properties from its shared ctx for (let key of Object.keys(test.ctx || {})) { - const value = test.ctx[key]; + const value = test.ctx[key] + if (_.isObject(value) && !mochaCtxKeysRe.test(key)) { //# nuke any object properties that come from //# cy.as() aliases or anything set from 'this' //# so we aggressively perform GC and prevent obj //# ref's from building up - test.ctx[key] = undefined; + test.ctx[key] = undefined } } //# reset the fn to be empty function //# for GC to be aggressive and prevent //# closures from hold references - test.fn = function() {}; + test.fn = function () {} //# prevent loop comprehension - return null; + return null } -}; +} -const setTestTimingsForHook = function(test, hookName, obj) { - if (test.timings == null) { test.timings = {}; } - if (test.timings[hookName] == null) { test.timings[hookName] = []; } - return test.timings[hookName].push(obj); -}; +const setTestTimingsForHook = function (test, hookName, obj) { + if (test.timings == null) { + test.timings = {} + } + + if (test.timings[hookName] == null) { + test.timings[hookName] = [] + } -const setTestTimings = function(test, name, obj) { - if (test.timings == null) { test.timings = {}; } - return test.timings[name] = obj; -}; + return test.timings[hookName].push(obj) +} -var setWallClockDuration = test => test.wallClockDuration = new Date() - test.wallClockStartedAt; +const setTestTimings = function (test, name, obj) { + if (test.timings == null) { + test.timings = {} + } -const reduceProps = (obj, props) => - _.reduce(props, function(memo, prop) { + test.timings[name] = obj +} + +const setWallClockDuration = (test) => { + return test.wallClockDuration = new Date() - test.wallClockStartedAt +} + +const reduceProps = (obj, props) => { + return _.reduce(props, (memo, prop) => { if (_.has(obj, prop) || (obj[prop] !== undefined)) { - memo[prop] = obj[prop]; + memo[prop] = obj[prop] } - return memo; + + return memo } - , {}) -; - -var wrap = runnable => - //# we need to optimize wrap by converting - //# tests to an id-based object which prevents - //# us from recursively iterating through every - //# parent since we could just return the found test - reduceProps(runnable, RUNNABLE_PROPS) -; - -const wrapAll = runnable => - _.extend( + , {}) +} + +const wrap = (runnable) => +//# we need to optimize wrap by converting +//# tests to an id-based object which prevents +//# us from recursively iterating through every +//# parent since we could just return the found test +{ + return reduceProps(runnable, RUNNABLE_PROPS) +} + +const wrapAll = (runnable) => { + return _.extend( {}, reduceProps(runnable, RUNNABLE_PROPS), reduceProps(runnable, RUNNABLE_LOGS) ) -; +} -const wrapErr = err => reduceProps(err, ERROR_PROPS); +const wrapErr = (err) => { + return reduceProps(err, ERROR_PROPS) +} -const getHookName = function(hook) { +const getHookName = function (hook) { //# find the name of the hook by parsing its //# title and pulling out whats between the quotes - const name = hook.title.match(betweenQuotesRe); - return name && name[1]; -}; + const name = hook.title.match(betweenQuotesRe) + + return name && name[1] +} -const forceGc = function(obj) { +const forceGc = function (obj) { //# aggressively forces GC by purging //# references to ctx, and removes callback //# functions for closures for (let key of Object.keys(obj.ctx || {})) { - const value = obj.ctx[key]; - obj.ctx[key] = undefined; + const value = obj.ctx[key] + + obj.ctx[key] = undefined } if (obj.fn) { - return obj.fn = function() {}; + obj.fn = function () {} } -}; +} -var anyTestInSuite = function(suite, fn) { +const anyTestInSuite = function (suite, fn) { for (let test of suite.tests) { - if (fn(test) === true) { return true; } + if (fn(test) === true) { + return true + } } for (suite of suite.suites) { - if (anyTestInSuite(suite, fn) === true) { return true; } + if (anyTestInSuite(suite, fn) === true) { + return true + } } //# else return false - return false; -}; + return false +} -const eachHookInSuite = function(suite, fn) { +const eachHookInSuite = function (suite, fn) { for (let type of HOOKS) { for (let hook of suite[`_${type}`]) { - fn(hook); + fn(hook) } } //# prevent loop comprehension - return null; -}; + return null +} -var onFirstTest = function(suite, fn) { - for (var test of suite.tests) { - if (fn(test)) { return test; } +const onFirstTest = function (suite, fn) { + for (const test of suite.tests) { + if (fn(test)) { + return test + } } for (suite of suite.suites) { - if (test = onFirstTest(suite, fn)) { return test; } + if (test = onFirstTest(suite, fn)) { + return test + } } -}; +} -const getAllSiblingTests = function(suite, getTestById) { - const tests = []; - suite.eachTest(test => { +const getAllSiblingTests = function (suite, getTestById) { + const tests = [] + + suite.eachTest((test) => { //# iterate through each of our suites tests. //# this will iterate through all nested tests //# as well. and then we add it only if its //# in our grepp'd tests array if (getTestById(test.id)) { - return tests.push(test); + return tests.push(test) } - }); + }) - return tests; -}; + return tests +} -const getTestFromHook = function(hook, suite, getTestById) { +const getTestFromHook = function (hook, suite, getTestById) { //# if theres already a currentTest use that - let found, test; - if (test = hook != null ? hook.ctx.currentTest : undefined) { return test; } + let found, test + + if (test = hook != null ? hook.ctx.currentTest : undefined) { + return test + } //# if we have a hook id then attempt //# to find the test by its id if (hook != null ? hook.id : undefined) { - found = onFirstTest(suite, test => { - return hook.id === test.id; - }); + found = onFirstTest(suite, (test) => { + return hook.id === test.id + }) - if (found) { return found; } + if (found) { + return found + } } //# returns us the very first test //# which is in our grepped tests array //# based on walking down the current suite //# iterating through each test until it matches - found = onFirstTest(suite, test => { - return getTestById(test.id); - }); + found = onFirstTest(suite, (test) => { + return getTestById(test.id) + }) - if (found) { return found; } + if (found) { + return found + } //# have one last final fallback where //# we just return true on the very first //# test (used in testing) - return onFirstTest(suite, test => true); -}; + return onFirstTest(suite, (test) => { + return true + }) +} //# we have to see if this is the last suite amongst //# its siblings. but first we have to filter out //# suites which dont have a grep'd test in them -const isLastSuite = function(suite, tests) { - if (suite.root) { return false; } +const isLastSuite = function (suite, tests) { + if (suite.root) { + return false + } //# grab all of the suites from our grep'd tests //# including all of their ancestor suites! - const suites = _.reduce(tests, function(memo, test) { - let parent; + const suites = _.reduce(tests, (memo, test) => { + let parent + while ((parent = test.parent)) { - memo.push(parent); - test = parent; + memo.push(parent) + test = parent } - return memo; + + return memo } - , []); + , []) //# intersect them with our parent suites and see if the last one is us return _ @@ -295,72 +356,78 @@ const isLastSuite = function(suite, tests) { .uniq() .intersection(suite.parent.suites) .last() - .value() === suite; -}; + .value() === suite +} //# we are the last test that will run in the suite //# if we're the last test in the tests array or //# if we failed from a hook and that hook was 'before' //# since then mocha skips the remaining tests in the suite -const lastTestThatWillRunInSuite = (test, tests) => isLastTest(test, tests) || (test.failedFromHookId && (test.hookName === "before all")); +const lastTestThatWillRunInSuite = (test, tests) => { + return isLastTest(test, tests) || (test.failedFromHookId && (test.hookName === 'before all')) +} -var isLastTest = (test, tests) => test === _.last(tests); +const isLastTest = (test, tests) => { + return test === _.last(tests) +} -const isRootSuite = suite => suite && suite.root; +const isRootSuite = (suite) => { + return suite && suite.root +} -const overrideRunnerHook = function(Cypress, _runner, getTestById, getTest, setTest, getTests) { +const overrideRunnerHook = function (Cypress, _runner, getTestById, getTest, setTest, getTests) { //# bail if our _runner doesnt have a hook. //# useful in tests - if (!_runner.hook) { return; } + if (!_runner.hook) { + return + } //# monkey patch the hook event so we can wrap //# 'test:after:run' around all of //# the hooks surrounding a test runnable - const _runnerHook = _runner.hook; + const _runnerHook = _runner.hook - return _runner.hook = function(name, fn) { - const hooks = this.suite[`_${name}`]; + _runner.hook = function (name, fn) { + const hooks = this.suite[`_${name}`] - const allTests = getTests(); + const allTests = getTests() - const changeFnToRunAfterHooks = function() { - const originalFn = fn; + const changeFnToRunAfterHooks = function () { + const originalFn = fn - const test = getTest(); + const test = getTest() - //# reset fn to invoke the hooks - //# first but before calling next(err) - //# we fire our events - return fn = function() { - setTest(null); + fn = function () { + setTest(null) - testAfterRun(test, Cypress); + testAfterRun(test, Cypress) //# and now invoke next(err) - return originalFn.apply(null, arguments); - }; - }; + return originalFn(...arguments) + } + } switch (name) { - case "afterEach": - var t = getTest(); + case 'afterEach': + const t = getTest() //# find all of the grep'd _tests which share //# the same parent suite as our current _test - var tests = getAllSiblingTests(t.parent, getTestById); + const tests = getAllSiblingTests(t.parent, getTestById) //# make sure this test isnt the last test overall but also //# isnt the last test in our grep'd parent suite's tests array if (this.suite.root && (t !== _.last(allTests)) && (t !== _.last(tests))) { - changeFnToRunAfterHooks(); + changeFnToRunAfterHooks() } - break; - case "afterAll": + break + + case 'afterAll': //# find all of the grep'd allTests which share //# the same parent suite as our current _test if (t = getTest()) { - const siblings = getAllSiblingTests(t.parent, getTestById); + const siblings = getAllSiblingTests(t.parent, getTestById) //# 1. if we're the very last test in the entire allTests //# we wait until the root suite fires @@ -370,126 +437,136 @@ const overrideRunnerHook = function(Cypress, _runner, getTestById, getTest, setT //# 3. else if we arent the last nested suite we fire if we're //# the last test that will run if ( - (isRootSuite(this.suite) && isLastTest(t, allTests)) || + (isRootSuite(this.suite) && isLastTest(t, allTests)) || (isRootSuite(this.suite.parent) && lastTestThatWillRunInSuite(t, siblings)) || (!isLastSuite(this.suite, allTests) && lastTestThatWillRunInSuite(t, siblings)) - ) { - changeFnToRunAfterHooks(); + ) { + changeFnToRunAfterHooks() } } - break; + + break } - return _runnerHook.call(this, name, fn); - }; -}; + return _runnerHook.call(this, name, fn) + } +} -const matchesGrep = function(runnable, grep) { +const matchesGrep = function (runnable, grep) { //# we have optimized this iteration to the maximum. //# we memoize the existential matchesGrep property //# so we dont regex again needlessly when going //# through tests which have already been set earlier if (((runnable.matchesGrep == null)) || (!_.isEqual(runnable.grepRe, grep))) { - runnable.grepRe = grep; - runnable.matchesGrep = grep.test(runnable.fullTitle()); + runnable.grepRe = grep + runnable.matchesGrep = grep.test(runnable.fullTitle()) } - return runnable.matchesGrep; -}; + return runnable.matchesGrep +} + +const getTestResults = (tests) => { + return _.map(tests, (test) => { + const obj = _.pick(test, 'id', 'duration', 'state') -const getTestResults = tests => - _.map(tests, function(test) { - const obj = _.pick(test, "id", "duration", "state"); - obj.title = test.originalTitle; + obj.title = test.originalTitle //# TODO FIX THIS! if (!obj.state) { - obj.state = "skipped"; + obj.state = 'skipped' } - return obj; + + return obj }) -; +} -const normalizeAll = function(suite, initialTests = {}, grep, setTestsById, setTests, onRunnable, onLogsById, getTestId) { - let hasTests = false; +const normalizeAll = function (suite, initialTests = {}, grep, setTestsById, setTests, onRunnable, onLogsById, getTestId) { + let hasTests = false //# only loop until we find the first test - onFirstTest(suite, test => hasTests = true); + onFirstTest(suite, (test) => { + return hasTests = true + }) //# if we dont have any tests then bail - if (!hasTests) { return; } + if (!hasTests) { + return + } //# we are doing a super perf loop here where //# we hand back a normalized object but also //# create optimized lookups for the tests without //# traversing through it multiple times - const tests = {}; - const grepIsDefault = _.isEqual(grep, defaultGrepRe); + const tests = {} + const grepIsDefault = _.isEqual(grep, defaultGrepRe) - const obj = normalize(suite, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId); + const obj = normalize(suite, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) if (setTestsById) { //# use callback here to hand back //# the optimized tests - setTestsById(tests); + setTestsById(tests) } if (setTests) { //# same pattern here - setTests(_.values(tests)); + setTests(_.values(tests)) } - return obj; -}; + return obj +} + +const normalize = function (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) { + const normalizer = (runnable) => { + let i -var normalize = function(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) { - const normalizer = runnable => { - let i; - runnable.id = getTestId(); + runnable.id = getTestId() //# tests have a type of 'test' whereas suites do not have a type property - if (runnable.type == null) { runnable.type = "suite"; } + if (runnable.type == null) { + runnable.type = 'suite' + } if (onRunnable) { - onRunnable(runnable); + onRunnable(runnable) } //# if we have a runnable in the initial state //# then merge in existing properties into the runnable if (i = initialTests[runnable.id]) { - _.each(RUNNABLE_LOGS, type => { - return _.each(i[type], onLogsById); - }); + _.each(RUNNABLE_LOGS, (type) => { + return _.each(i[type], onLogsById) + }) - _.extend(runnable, i); + _.extend(runnable, i) } //# reduce this runnable down to its props //# and collections - return wrapAll(runnable); - }; + return wrapAll(runnable) + } - const push = test => { - return tests[test.id] != null ? tests[test.id] : (tests[test.id] = test); - }; + const push = (test) => { + return tests[test.id] != null ? tests[test.id] : (tests[test.id] = test) + } - const obj = normalizer(runnable); + const obj = normalizer(runnable) //# if we have a default grep then avoid //# grepping altogether and just push //# tests into the array of tests if (grepIsDefault) { - if (runnable.type === "test") { - push(runnable); + if (runnable.type === 'test') { + push(runnable) } //# and recursively iterate and normalize all other _runnables - _.each({tests: runnable.tests, suites: runnable.suites}, (_runnables, key) => { + _.each({ tests: runnable.tests, suites: runnable.suites }, (_runnables, key) => { if (runnable[key]) { - return obj[key] = _.map(_runnables, runnable => { - return normalize(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId); - }); + obj[key] = _.map(_runnables, (runnable) => { + return normalize(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) + }) } - }); + }) } else { //# iterate through all tests and only push them in //# if they match the current grep @@ -497,20 +574,21 @@ var normalize = function(runnable, tests, initialTests, grep, grepIsDefault, onR //# only push in the test if it matches //# our grep if (matchesGrep(test, grep)) { - memo.push(normalizer(test)); - push(test); + memo.push(normalizer(test)) + push(test) } - return memo; + + return memo } - , []); + , []) //# and go through the suites obj.suites = _.reduce(runnable.suites != null ? runnable.suites : [], (memo, suite) => { //# but only add them if a single nested test //# actually matches the grep - const any = anyTestInSuite(suite, test => { - return matchesGrep(test, grep); - }); + const any = anyTestInSuite(suite, (test) => { + return matchesGrep(test, grep) + }) if (any) { memo.push( @@ -524,187 +602,209 @@ var normalize = function(runnable, tests, initialTests, grep, grepIsDefault, onR onLogsById, getTestId ) - ); + ) } - return memo; + return memo } - , []); + , []) } - return obj; -}; + return obj +} -const afterEachFailed = function(Cypress, test, err) { - test.state = "failed"; - test.err = wrapErr(err); +const afterEachFailed = function (Cypress, test, err) { + test.state = 'failed' + test.err = wrapErr(err) - return Cypress.action("runner:test:end", wrap(test)); -}; + return Cypress.action('runner:test:end', wrap(test)) +} -const hookFailed = function(hook, err, hookName, getTestById, getTest) { +const hookFailed = function (hook, err, hookName, getTestById, getTest) { //# finds the test by returning the first test from //# the parent or looping through the suites until //# it finds the first test - const test = getTest() || getTestFromHook(hook, hook.parent, getTestById); - test.err = err; - test.state = "failed"; - test.duration = hook.duration; //# TODO: nope (?) - test.hookName = hookName; //# TODO: why are we doing this? - test.failedFromHookId = hook.hookId; + const test = getTest() || getTestFromHook(hook, hook.parent, getTestById) + + test.err = err + test.state = 'failed' + test.duration = hook.duration //# TODO: nope (?) + test.hookName = hookName //# TODO: why are we doing this? + test.failedFromHookId = hook.hookId if (hook.alreadyEmittedMocha) { - //# TODO: won't this always hit right here??? - //# when would the hook not be emitted at this point? - return test.alreadyEmittedMocha = true; + test.alreadyEmittedMocha = true } else { - return Cypress.action("runner:test:end", wrap(test)); + return Cypress.action('runner:test:end', wrap(test)) } -}; +} -const _runnerListeners = function(_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) { - _runner.on("start", () => - Cypress.action("runner:start", { - start: new Date() +const _runnerListeners = function (_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) { + _runner.on('start', () => { + return Cypress.action('runner:start', { + start: new Date(), }) - ); + } + ) - _runner.on("end", () => - Cypress.action("runner:end", { - end: new Date() + _runner.on('end', () => { + return Cypress.action('runner:end', { + end: new Date(), }) - ); + } + ) - _runner.on("suite", function(suite) { - if (_emissions.started[suite.id]) { return; } + _runner.on('suite', (suite) => { + if (_emissions.started[suite.id]) { + return + } - _emissions.started[suite.id] = true; + _emissions.started[suite.id] = true - return Cypress.action("runner:suite:start", wrap(suite)); - }); + return Cypress.action('runner:suite:start', wrap(suite)) + }) - _runner.on("suite end", function(suite) { + _runner.on('suite end', (suite) => { //# cleanup our suite + its hooks - forceGc(suite); - eachHookInSuite(suite, forceGc); + forceGc(suite) + eachHookInSuite(suite, forceGc) - if (_emissions.ended[suite.id]) { return; } + if (_emissions.ended[suite.id]) { + return + } - _emissions.ended[suite.id] = true; + _emissions.ended[suite.id] = true - return Cypress.action("runner:suite:end", wrap(suite)); - }); + return Cypress.action('runner:suite:end', wrap(suite)) + }) - _runner.on("hook", function(hook) { - if (hook.hookId == null) { hook.hookId = getHookId(); } - if (hook.hookName == null) { hook.hookName = getHookName(hook); } + _runner.on('hook', (hook) => { + if (hook.hookId == null) { + hook.hookId = getHookId() + } + + if (hook.hookName == null) { + hook.hookName = getHookName(hook) + } //# mocha incorrectly sets currentTest on before all's. //# if there is a nested suite with a before, then //# currentTest will refer to the previous test run //# and not our current - if ((hook.hookName === "before all") && hook.ctx.currentTest) { - delete hook.ctx.currentTest; + if ((hook.hookName === 'before all') && hook.ctx.currentTest) { + delete hook.ctx.currentTest } //# set the hook's id from the test because //# hooks do not have their own id, their //# commands need to grouped with the test //# and we can only associate them by this id - const test = getTest() || getTestFromHook(hook, hook.parent, getTestById); - hook.id = test.id; - hook.ctx.currentTest = test; + const test = getTest() || getTestFromHook(hook, hook.parent, getTestById) + + hook.id = test.id + hook.ctx.currentTest = test //# make sure we set this test as the current now //# else its possible that our TEST_AFTER_RUN_EVENT //# will never fire if this failed in a before hook - setTest(test); + setTest(test) - return Cypress.action("runner:hook:start", wrap(hook)); - }); + return Cypress.action('runner:hook:start', wrap(hook)) + }) - _runner.on("hook end", hook => Cypress.action("runner:hook:end", wrap(hook))); + _runner.on('hook end', (hook) => { + return Cypress.action('runner:hook:end', wrap(hook)) + }) - _runner.on("test", function(test) { - setTest(test); + _runner.on('test', (test) => { + setTest(test) - if (_emissions.started[test.id]) { return; } + if (_emissions.started[test.id]) { + return + } - _emissions.started[test.id] = true; + _emissions.started[test.id] = true - return Cypress.action("runner:test:start", wrap(test)); - }); + return Cypress.action('runner:test:start', wrap(test)) + }) - _runner.on("test end", function(test) { - if (_emissions.ended[test.id]) { return; } + _runner.on('test end', (test) => { + if (_emissions.ended[test.id]) { + return + } - _emissions.ended[test.id] = true; + _emissions.ended[test.id] = true - return Cypress.action("runner:test:end", wrap(test)); - }); + return Cypress.action('runner:test:end', wrap(test)) + }) - _runner.on("pass", test => Cypress.action("runner:pass", wrap(test))); + _runner.on('pass', (test) => { + return Cypress.action('runner:pass', wrap(test)) + }) //# if a test is pending mocha will only //# emit the pending event instead of the test //# so we normalize the pending / test events - _runner.on("pending", function(test) { + _runner.on('pending', function (test) { //# do nothing if our test is skipped - if (test._ALREADY_RAN) { return; } + if (test._ALREADY_RAN) { + return + } if (!fired(TEST_BEFORE_RUN_EVENT, test)) { - fire(TEST_BEFORE_RUN_EVENT, test, Cypress); + fire(TEST_BEFORE_RUN_EVENT, test, Cypress) } - test.state = "pending"; + test.state = 'pending' if (!test.alreadyEmittedMocha) { //# do not double emit this event - test.alreadyEmittedMocha = true; + test.alreadyEmittedMocha = true - Cypress.action("runner:pending", wrap(test)); + Cypress.action('runner:pending', wrap(test)) } - this.emit("test", test); + this.emit('test', test) //# if this is not the last test amongst its siblings //# then go ahead and fire its test:after:run event //# else this will not get called - const tests = getAllSiblingTests(test.parent, getTestById); + const tests = getAllSiblingTests(test.parent, getTestById) if (_.last(tests) !== test) { - return fire(TEST_AFTER_RUN_EVENT, test, Cypress); + return fire(TEST_AFTER_RUN_EVENT, test, Cypress) } - }); + }) - return _runner.on("fail", function(runnable, err) { - let hookName; - const isHook = runnable.type === "hook"; + return _runner.on('fail', (runnable, err) => { + let hookName + const isHook = runnable.type === 'hook' if (isHook) { - const parentTitle = runnable.parent.title; - hookName = getHookName(runnable); + const parentTitle = runnable.parent.title + + hookName = getHookName(runnable) //# append a friendly message to the error indicating //# we're skipping the remaining tests in this suite err = $utils.appendErrMsg( err, - $utils.errMessageByPath("uncaught.error_in_hook", { + $utils.errMessageByPath('uncaught.error_in_hook', { parentTitle, - hookName + hookName, }) - ); + ) } //# always set runnable err so we can tap into //# taking a screenshot on error - runnable.err = wrapErr(err); + runnable.err = wrapErr(err) if (!runnable.alreadyEmittedMocha) { //# do not double emit this event - runnable.alreadyEmittedMocha = true; + runnable.alreadyEmittedMocha = true - Cypress.action("runner:fail", wrap(runnable), runnable.err); + Cypress.action('runner:fail', wrap(runnable), runnable.err) } //# if we've already fired the test after run event @@ -712,145 +812,168 @@ const _runnerListeners = function(_runner, Cypress, _emissions, getTestById, get //# a double done(err) callback, and we need to fire //# this again! if (fired(TEST_AFTER_RUN_EVENT, runnable)) { - fire(TEST_AFTER_RUN_EVENT, runnable, Cypress); + fire(TEST_AFTER_RUN_EVENT, runnable, Cypress) } if (isHook) { //# if a hook fails (such as a before) then the test will never //# get run and we'll need to make sure we set the test so that //# the TEST_AFTER_RUN_EVENT fires correctly - return hookFailed(runnable, runnable.err, hookName, getTestById, getTest); + return hookFailed(runnable, runnable.err, hookName, getTestById, getTest) } - }); -}; + }) +} -const create = function(specWindow, mocha, Cypress, cy) { - let _id = 0; - let _hookId = 0; - let _uncaughtFn = null; +const create = function (specWindow, mocha, Cypress, cy) { + let _id = 0 + let _hookId = 0 + let _uncaughtFn = null - const _runner = mocha.getRunner(); - _runner.suite = mocha.getRootSuite(); + const _runner = mocha.getRunner() - specWindow.onerror = function() { - let err = cy.onSpecWindowUncaughtException.apply(cy, arguments); + _runner.suite = mocha.getRootSuite() + + specWindow.onerror = function () { + let err = cy.onSpecWindowUncaughtException(...arguments) //# err will not be returned if cy can associate this //# uncaught exception to an existing runnable - if (!err) { return true; } + if (!err) { + return true + } - const todoMsg = function() { - if (!Cypress.config("isTextTerminal")) { - return "Check your console for the stack trace or click this message to see where it originated from."; + const todoMsg = function () { + if (!Cypress.config('isTextTerminal')) { + return 'Check your console for the stack trace or click this message to see where it originated from.' } - }; + } - const append = () => - _.chain([ - "Cypress could not associate this error to any specific test.", - "We dynamically generated a new test to display this failure.", - todoMsg() + const append = () => { + return _.chain([ + 'Cypress could not associate this error to any specific test.', + 'We dynamically generated a new test to display this failure.', + todoMsg(), ]) .compact() - .join("\n\n") - ; + .join('\n\n') + } //# else do the same thing as mocha here - err = $utils.appendErrMsg(err, append()); + err = $utils.appendErrMsg(err, append()) - const throwErr = function() { - throw err; - }; + const throwErr = function () { + throw err + } //# we could not associate this error //# and shouldn't ever start our run - _uncaughtFn = throwErr; + _uncaughtFn = throwErr //# return undefined so the browser does its default //# uncaught exception behavior (logging to console) - return undefined; - }; + return undefined + } //# hold onto the _runnables for faster lookup later - let _stopped = false; - let _test = null; - let _tests = []; - let _testsById = {}; - const _testsQueue = []; - const _testsQueueById = {}; - const _runnables = []; - const _logsById = {}; + let _stopped = false + let _test = null + let _tests = [] + let _testsById = {} + const _testsQueue = [] + const _testsQueueById = {} + const _runnables = [] + const _logsById = {} let _emissions = { started: {}, - ended: {} - }; - let _startTime = null; + ended: {}, + } + let _startTime = null const getTestId = () => - //# increment the id counter - `r${_id += 1}` - ; + //# increment the id counter + { + return `r${_id += 1}` + } - const getHookId = () => `h${_hookId += 1}`; + const getHookId = () => { + return `h${_hookId += 1}` + } - const setTestsById = tbid => _testsById = tbid; + const setTestsById = (tbid) => { + return _testsById = tbid + } - const setTests = t => _tests = t; + const setTests = (t) => { + return _tests = t + } - const getTests = () => _tests; + const getTests = () => { + return _tests + } - const onRunnable = r => _runnables.push(r); + const onRunnable = (r) => { + return _runnables.push(r) + } - const onLogsById = l => _logsById[l.id] = l; + const onLogsById = (l) => { + return _logsById[l.id] = l + } - const getTest = () => _test; + const getTest = () => { + return _test + } - const setTest = t => _test = t; + const setTest = (t) => { + return _test = t + } - const getTestById = function(id) { + const getTestById = function (id) { //# perf short circuit - if (!id) { return; } + if (!id) { + return + } - return _testsById[id]; - }; + return _testsById[id] + } - overrideRunnerHook(Cypress, _runner, getTestById, getTest, setTest, getTests); + overrideRunnerHook(Cypress, _runner, getTestById, getTest, setTest, getTests) return { - grep(re) { + grep (re) { if (arguments.length) { - return _runner._grep = re; + _runner._grep = re } else { //# grab grep from the mocha _runner //# or just set it to all in case //# there is a mocha regression - return _runner._grep != null ? _runner._grep : (_runner._grep = defaultGrepRe); + return _runner._grep != null ? _runner._grep : (_runner._grep = defaultGrepRe) } }, - options(options = {}) { + options (options = {}) { //# TODO //# need to handle //# ignoreLeaks, asyncOnly, globals - let re; + let re + if (re = options.grep) { - return this.grep(re); + return this.grep(re) } }, - normalizeAll(tests) { + normalizeAll (tests) { //# if we have an uncaught error then slice out //# all of the tests and suites and just generate //# a single test since we received an uncaught //# error prior to processing any of mocha's tests //# which could have occurred in a separate support file if (_uncaughtFn) { - _runner.suite.suites = []; - _runner.suite.tests = []; + _runner.suite.suites = [] + _runner.suite.tests = [] //# create a runnable to associate for the failure - mocha.createRootTest("An uncaught error was detected outside of a test", _uncaughtFn); + mocha.createRootTest('An uncaught error was detected outside of a test', _uncaughtFn) } return normalizeAll( @@ -862,161 +985,167 @@ const create = function(specWindow, mocha, Cypress, cy) { onRunnable, onLogsById, getTestId - ); + ) }, - run(fn) { - if (_startTime == null) { _startTime = moment().toJSON(); } + run (fn) { + if (_startTime == null) { + _startTime = moment().toJSON() + } - _runnerListeners(_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId); + _runnerListeners(_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) - return _runner.run(function(failures) { + return _runner.run((failures) => { //# if we happen to make it all the way through //# the run, then just set _stopped to true here - _stopped = true; + _stopped = true //# TODO this functions is not correctly //# synchronized with the 'end' event that //# we manage because of uncaught hook errors if (fn) { - return fn(failures, getTestResults(_tests)); + return fn(failures, getTestResults(_tests)) } - }); + }) }, - onRunnableRun(runnableRun, runnable, args) { - let lifecycleStart, test; + onRunnableRun (runnableRun, runnable, args) { + let lifecycleStart, test + if (!runnable.id) { - throw new Error("runnable must have an id", runnable.id); + throw new Error('runnable must have an id', runnable.id) } switch (runnable.type) { - case "hook": - test = getTest() || getTestFromHook(runnable, runnable.parent, getTestById); - break; + case 'hook': + test = getTest() || getTestFromHook(runnable, runnable.parent, getTestById) + break - case "test": - test = runnable; - break; + case 'test': + test = runnable + break } //# closure for calculating the actual //# runtime of a runnables fn exection duration //# and also the run of the runnable:after:run:async event - let wallClockStartedAt = null; - let wallClockEnd = null; - let fnDurationStart = null; - let fnDurationEnd = null; - let afterFnDurationStart = null; - let afterFnDurationEnd = null; + let wallClockStartedAt = null + let wallClockEnd = null + let fnDurationStart = null + let fnDurationEnd = null + let afterFnDurationStart = null + let afterFnDurationEnd = null //# when this is a hook, capture the real start //# date so we can calculate our test's duration //# including all of its hooks - wallClockStartedAt = new Date(); + wallClockStartedAt = new Date() if (!test.wallClockStartedAt) { //# if we don't have lifecycle timings yet - lifecycleStart = wallClockStartedAt; + lifecycleStart = wallClockStartedAt } - if (test.wallClockStartedAt == null) { test.wallClockStartedAt = wallClockStartedAt; } + if (test.wallClockStartedAt == null) { + test.wallClockStartedAt = wallClockStartedAt + } //# if this isnt a hook, then the name is 'test' - const hookName = runnable.type === "hook" ? getHookName(runnable) : "test"; + const hookName = runnable.type === 'hook' ? getHookName(runnable) : 'test' //# if we haven't yet fired this event for this test //# that means that we need to reset the previous state //# of cy - since we now have a new 'test' and all of the //# associated _runnables will share this state if (!fired(TEST_BEFORE_RUN_EVENT, test)) { - fire(TEST_BEFORE_RUN_EVENT, test, Cypress); + fire(TEST_BEFORE_RUN_EVENT, test, Cypress) } //# extract out the next(fn) which mocha uses to //# move to the next runnable - this will be our async seam - const _next = args[0]; + const _next = args[0] - const next = function(err) { + const next = function (err) { //# now set the duration of the after runnable run async event - afterFnDurationEnd = (wallClockEnd = new Date()); + afterFnDurationEnd = (wallClockEnd = new Date()) switch (runnable.type) { - case "hook": + case 'hook': //# reset runnable duration to include lifecycle //# and afterFn timings purely for the mocha runner. //# this is what it 'feels' like to the user - runnable.duration = wallClockEnd - wallClockStartedAt; + runnable.duration = wallClockEnd - wallClockStartedAt setTestTimingsForHook(test, hookName, { hookId: runnable.hookId, fnDuration: fnDurationEnd - fnDurationStart, - afterFnDuration: afterFnDurationEnd - afterFnDurationStart - }); - break; + afterFnDuration: afterFnDurationEnd - afterFnDurationStart, + }) + break - case "test": + case 'test': //# if we are currently on a test then //# recalculate its duration to be based //# against that (purely for the mocha reporter) - test.duration = wallClockEnd - test.wallClockStartedAt; + test.duration = wallClockEnd - test.wallClockStartedAt //# but still preserve its actual function //# body duration for timings - setTestTimings(test, "test", { + setTestTimings(test, 'test', { fnDuration: fnDurationEnd - fnDurationStart, - afterFnDuration: afterFnDurationEnd - afterFnDurationStart - }); - break; + afterFnDuration: afterFnDurationEnd - afterFnDurationStart, + }) + break } - return _next(err); - }; + return _next(err) + } - const onNext = function(err) { + const onNext = function (err) { //# when done with the function set that to end - fnDurationEnd = new Date(); + fnDurationEnd = new Date() //# and also set the afterFnDuration to this same date - afterFnDurationStart = fnDurationEnd; + afterFnDurationStart = fnDurationEnd //# attach error right now //# if we have one if (err) { if (err instanceof Pending) { - err.isPending = true; + err.isPending = true } - runnable.err = wrapErr(err); + runnable.err = wrapErr(err) } return runnableAfterRunAsync(runnable, Cypress) - .then(function() { + .then(() => { //# once we complete callback with the //# original err - next(err); + next(err) //# return null here to signal to bluebird //# that we did not forget to return a promise //# because mocha internally does not return //# the test.run(fn) - return null;}).catch(function(err) { - next(err); + return null + }).catch((err) => { + next(err) //# return null here to signal to bluebird //# that we did not forget to return a promise //# because mocha internally does not return //# the test.run(fn) - return null; - }); - }; + return null + }) + } //# our runnable is about to run, so let cy know. this enables //# us to always have a correct runnable set even when we are //# running lifecycle events //# and also get back a function result handler that we use as //# an async seam - cy.setRunnable(runnable, hookName); + cy.setRunnable(runnable, hookName) //# TODO: handle promise timeouts here! //# whenever any runnable is about to run @@ -1025,190 +1154,207 @@ const create = function(specWindow, mocha, Cypress, cy) { //# test:before:run:async action if its not //# been fired before for this test return testBeforeRunAsync(test, Cypress) - .catch(function(err) { + .catch((err) => { //# TODO: if our async tasks fail //# then allow us to cause the test //# to fail here by blowing up its fn //# callback - const { fn } = runnable; + const { fn } = runnable - const restore = () => runnable.fn = fn; + const restore = () => { + return runnable.fn = fn + } - return runnable.fn = function() { - restore(); + runnable.fn = function () { + restore() - throw err; - };}).finally(function() { + throw err + } + }).finally(() => { if (lifecycleStart) { //# capture how long the lifecycle took as part //# of the overall wallClockDuration of our test - setTestTimings(test, "lifecycle", new Date() - lifecycleStart); + setTestTimings(test, 'lifecycle', new Date() - lifecycleStart) } //# capture the moment we're about to invoke //# the runnable's callback function - fnDurationStart = new Date(); + fnDurationStart = new Date() //# call the original method with our //# custom onNext function - return runnableRun.call(runnable, onNext); - }); + return runnableRun.call(runnable, onNext) + }) }, - getStartTime() { - return _startTime; + getStartTime () { + return _startTime }, - setStartTime(iso) { - return _startTime = iso; + setStartTime (iso) { + _startTime = iso }, - countByTestState(tests, state) { - const count = _.filter(tests, (test, key) => test.state === state); + countByTestState (tests, state) { + const count = _.filter(tests, (test, key) => { + return test.state === state + }) - return count.length; + return count.length }, - setNumLogs(num) { - return $Log.setCounter(num); + setNumLogs (num) { + return $Log.setCounter(num) }, - getEmissions() { - return _emissions; + getEmissions () { + return _emissions }, - getTestsState() { - const id = _test != null ? _test.id : undefined; - const tests = {}; + getTestsState () { + const id = _test != null ? _test.id : undefined + const tests = {} //# bail if we dont have a current test - if (!id) { return {}; } + if (!id) { + return {} + } //# search through all of the tests //# until we find the current test //# and break then for (var test of _tests) { if (test.id === id) { - break; + break } else { - test = wrapAll(test); + test = wrapAll(test) + + _.each(RUNNABLE_LOGS, (type) => { + let logs - _.each(RUNNABLE_LOGS, function(type) { - let logs; if (logs = test[type]) { - return test[type] = _.map(logs, $Log.toSerializedJSON); + test[type] = _.map(logs, $Log.toSerializedJSON) } - }); + }) - tests[test.id] = test; + tests[test.id] = test } } - return tests; + return tests }, - stop() { + stop () { if (_stopped) { - return; + return } - _stopped = true; + _stopped = true //# abort the run - _runner.abort(); + _runner.abort() //# emit the final 'end' event //# since our reporter depends on this event //# and mocha may never fire this becuase our //# runnable may never finish - _runner.emit("end"); + _runner.emit('end') //# remove all the listeners //# so no more events fire - return _runner.removeAllListeners(); + return _runner.removeAllListeners() }, getDisplayPropsForLog: $Log.getDisplayProps, - getConsolePropsForLogById(logId) { - let attrs; + getConsolePropsForLogById (logId) { + let attrs + if (attrs = _logsById[logId]) { - return $Log.getConsoleProps(attrs); + return $Log.getConsoleProps(attrs) } }, - getSnapshotPropsForLogById(logId) { - let attrs; + getSnapshotPropsForLogById (logId) { + let attrs + if (attrs = _logsById[logId]) { - return $Log.getSnapshotProps(attrs); + return $Log.getSnapshotProps(attrs) } }, - getErrorByTestId(testId) { - let test; + getErrorByTestId (testId) { + let test + if (test = getTestById(testId)) { - return wrapErr(test.err); + return wrapErr(test.err) } }, - resumeAtTest(id, emissions = {}) { - Cypress._RESUMED_AT_TEST = id; + resumeAtTest (id, emissions = {}) { + Cypress._RESUMED_AT_TEST = id - _emissions = emissions; + _emissions = emissions for (let test of _tests) { if (test.id !== id) { - test._ALREADY_RAN = true; - test.pending = true; + test._ALREADY_RAN = true + test.pending = true } else { //# bail so we can stop now - return; + return } } }, - cleanupQueue(numTestsKeptInMemory) { - var cleanup = function(queue) { + cleanupQueue (numTestsKeptInMemory) { + const cleanup = function (queue) { if (queue.length > numTestsKeptInMemory) { - const test = queue.shift(); + const test = queue.shift() - delete _testsQueueById[test.id]; + delete _testsQueueById[test.id] - _.each(RUNNABLE_LOGS, logs => - _.each(test[logs], function(attrs) { - //# we know our attrs have been cleaned - //# now, so lets store that - attrs._hasBeenCleanedUp = true; + _.each(RUNNABLE_LOGS, (logs) => { + return _.each(test[logs], (attrs) => { + //# we know our attrs have been cleaned + //# now, so lets store that + attrs._hasBeenCleanedUp = true - return $Log.reduceMemory(attrs); + return $Log.reduceMemory(attrs) }) - ); + } + ) - return cleanup(queue); + return cleanup(queue) } - }; + } - return cleanup(_testsQueue); + return cleanup(_testsQueue) }, - addLog(attrs, isInteractive) { + addLog (attrs, isInteractive) { //# we dont need to hold a log reference //# to anything in memory when we're headless //# because you cannot inspect any logs - let existing; - if (!isInteractive) { return; } + let existing + + if (!isInteractive) { + return + } - let test = getTestById(attrs.testId); + let test = getTestById(attrs.testId) //# bail if for whatever reason we //# cannot associate this log to a test - if (!test) { return; } + if (!test) { + return + } //# if this test isnt in the current queue //# then go ahead and add it if (!_testsQueueById[test.id]) { - _testsQueueById[test.id] = true; - _testsQueue.push(test); + _testsQueueById[test.id] = true + _testsQueue.push(test) } if (existing = _logsById[attrs.id]) { @@ -1217,28 +1363,31 @@ const create = function(specWindow, mocha, Cypress, cy) { //# we've already cleaned up these attrs //# and in that case we don't want to do //# anything at all - if (existing._hasBeenCleanedUp) { return; } + if (existing._hasBeenCleanedUp) { + return + } //# mutate the existing object - return _.extend(existing, attrs); - } else { - _logsById[attrs.id] = attrs; + return _.extend(existing, attrs) + } - const { testId, instrument } = attrs; + _logsById[attrs.id] = attrs - if (test = getTestById(testId)) { - //# pluralize the instrument - //# as a property on the runnable - let name; - const logs = test[name = instrument + "s"] != null ? test[name] : (test[name] = []); + const { testId, instrument } = attrs - //# else push it onto the logs - return logs.push(attrs); - } + if (test = getTestById(testId)) { + //# pluralize the instrument + //# as a property on the runnable + let name + const logs = test[name = `${instrument}s`] != null ? test[name] : (test[name] = []) + + //# else push it onto the logs + return logs.push(attrs) } - } - }; -}; + + }, + } +} module.exports = { overrideRunnerHook, @@ -1247,5 +1396,5 @@ module.exports = { normalizeAll, - create -}; + create, +} From 0183b7adcb8b94d67a0f17d5b13da2c0c4b98b55 Mon Sep 17 00:00:00 2001 From: decaffeinate <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:29:49 -0500 Subject: [PATCH 005/300] decaffeinate: Rename mocha.coffee from .coffee to .js --- packages/driver/src/cypress/{mocha.coffee => mocha.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/driver/src/cypress/{mocha.coffee => mocha.js} (100%) diff --git a/packages/driver/src/cypress/mocha.coffee b/packages/driver/src/cypress/mocha.js similarity index 100% rename from packages/driver/src/cypress/mocha.coffee rename to packages/driver/src/cypress/mocha.js From 9691af33f7780123cfddda99370bc9389174f545 Mon Sep 17 00:00:00 2001 From: decaffeinate <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:29:50 -0500 Subject: [PATCH 006/300] decaffeinate: Convert mocha.coffee to JS --- packages/driver/src/cypress/mocha.js | 361 ++++++++++++++------------- 1 file changed, 194 insertions(+), 167 deletions(-) diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index 21df770d7629..5e718c55646b 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -1,203 +1,230 @@ -_ = require("lodash") -$utils = require("./utils") - -## in the browser mocha is coming back -## as window -mocha = require("mocha") - -Mocha = mocha.Mocha ? mocha -Test = Mocha.Test -Runner = Mocha.Runner -Runnable = Mocha.Runnable - -runnerRun = Runner::run -runnerFail = Runner::fail -runnableRun = Runnable::run -runnableClearTimeout = Runnable::clearTimeout -runnableResetTimeout = Runnable::resetTimeout - -## don't let mocha polute the global namespace -delete window.mocha -delete window.Mocha - -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 = (name) -> - @_ui = Mocha.interfaces[name] - - if not @_ui - $utils.throwErrByPath("mocha.invalid_interface", { args: { name } }) - - @_ui = @_ui(@suite) - - ## this causes the mocha globals in the spec window to be defined - ## such as describe, it, before, beforeEach, etc - @suite.emit("pre-require", specWindow, null, @) - - return @ - - _mocha.ui("bdd") - -set = (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 - M = specWindow.Mocha = Mocha - 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 - ui(specWindow, _mocha) - -globals = (specWindow, reporter) -> - reporter ?= -> - - _mocha = new Mocha({ - reporter: reporter +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const $utils = require("./utils"); + +//# in the browser mocha is coming back +//# as window +const mocha = require("mocha"); + +const Mocha = mocha.Mocha != null ? mocha.Mocha : mocha; +const { Test } = Mocha; +const { Runner } = Mocha; +const { Runnable } = Mocha; + +const runnerRun = Runner.prototype.run; +const runnerFail = Runner.prototype.fail; +const runnableRun = Runnable.prototype.run; +const runnableClearTimeout = Runnable.prototype.clearTimeout; +const runnableResetTimeout = Runnable.prototype.resetTimeout; + +//# don't let mocha polute the global namespace +delete window.mocha; +delete window.Mocha; + +const ui = function(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) { + $utils.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); + + return this; + }; + + return _mocha.ui("bdd"); +}; + +const set = function(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 globals = function(specWindow, reporter) { + if (reporter == null) { reporter = function() {}; } + + const _mocha = new Mocha({ + reporter, enableTimeouts: false - }) - - ## set mocha props on the specWindow - set(specWindow, _mocha) - - ## return the newly created mocha instance - return _mocha - -getRunner = (_mocha) -> - Runner::run = -> - ## reset our runner#run function - ## so the next time we call it - ## its normal again! - restoreRunnerRun() - - ## return the runner instance - return @ + }); - _mocha.run() + //# set mocha props on the specWindow + set(specWindow, _mocha); -restoreRunnableClearTimeout = -> - Runnable::clearTimeout = runnableClearTimeout + //# return the newly created mocha instance + return _mocha; +}; -restoreRunnableResetTimeout = -> - Runnable::resetTimeout = runnableResetTimeout +const getRunner = function(_mocha) { + Runner.prototype.run = function() { + //# reset our runner#run function + //# so the next time we call it + //# its normal again! + restoreRunnerRun(); -restoreRunnerRun = -> - Runner::run = runnerRun + //# return the runner instance + return this; + }; -restoreRunnerFail = -> - Runner::fail = runnerFail + return _mocha.run(); +}; -restoreRunnableRun = -> - Runnable::run = runnableRun +const restoreRunnableClearTimeout = () => Runnable.prototype.clearTimeout = runnableClearTimeout; -patchRunnerFail = -> - ## matching the current Runner.prototype.fail except - ## changing the logic for determing whether this is a valid err - Runner::fail = (runnable, err) -> - ## if this isnt a correct error object then just bail - ## and call the original function - if Object.prototype.toString.call(err) isnt "[object Error]" - return runnerFail.call(@, runnable, err) +const restoreRunnableResetTimeout = () => Runnable.prototype.resetTimeout = runnableResetTimeout; - ## else replicate the normal mocha functionality - ++@failures +var restoreRunnerRun = () => Runner.prototype.run = runnerRun; - runnable.state = "failed" +const restoreRunnerFail = () => Runner.prototype.fail = runnerFail; - @emit("fail", runnable, err) +const restoreRunnableRun = () => Runnable.prototype.run = runnableRun; -patchRunnableRun = (Cypress) -> - Runnable::run = (args...) -> - runnable = @ +const patchRunnerFail = () => + //# matching the current Runner.prototype.fail except + //# changing the logic for determing whether this is a valid err + Runner.prototype.fail = function(runnable, err) { + //# 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); + } - Cypress.action("mocha:runnable:run", runnableRun, runnable, args) + //# else replicate the normal mocha functionality + ++this.failures; -patchRunnableClearTimeout = -> - Runnable::clearTimeout = -> - ## call the original - runnableClearTimeout.apply(@, arguments) + runnable.state = "failed"; - ## nuke the timer property - ## for testing purposes - @timer = null - -patchRunnableResetTimeout = -> - Runnable::resetTimeout = -> - runnable = @ + return this.emit("fail", runnable, err); + } +; - ms = @timeout() or 1e9 +const patchRunnableRun = Cypress => + Runnable.prototype.run = function(...args) { + const runnable = this; - @clearTimeout() + return Cypress.action("mocha:runnable:run", runnableRun, runnable, args); + } +; - getErrPath = -> - ## we've yield an explicit done callback - if runnable.async - "mocha.async_timed_out" - else - ## 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. - "mocha.timed_out" +const patchRunnableClearTimeout = () => + Runnable.prototype.clearTimeout = function() { + //# call the original + runnableClearTimeout.apply(this, arguments); - @timer = setTimeout -> - errMessage = $utils.errMessageByPath(getErrPath(), { ms }) - runnable.callback new Error(errMessage) - runnable.timedOut = true - , ms + //# nuke the timer property + //# for testing purposes + return this.timer = null; + } +; + +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"; + } else { + //# 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"; + } + }; + + return this.timer = setTimeout(function() { + const errMessage = $utils.errMessageByPath(getErrPath(), { ms }); + runnable.callback(new Error(errMessage)); + return runnable.timedOut = true; + } + , ms); + } +; -restore = -> - restoreRunnerRun() - restoreRunnerFail() - restoreRunnableRun() - restoreRunnableClearTimeout() - restoreRunnableResetTimeout() +const restore = function() { + restoreRunnerRun(); + restoreRunnerFail(); + restoreRunnableRun(); + restoreRunnableClearTimeout(); + return restoreRunnableResetTimeout(); +}; -override = (Cypress) -> - patchRunnerFail() - patchRunnableRun(Cypress) - patchRunnableClearTimeout() - patchRunnableResetTimeout() +const override = function(Cypress) { + patchRunnerFail(); + patchRunnableRun(Cypress); + patchRunnableClearTimeout(); + return patchRunnableResetTimeout(); +}; -create = (specWindow, Cypress, reporter) -> - restore() +const create = function(specWindow, Cypress, reporter) { + restore(); - override(Cypress) + override(Cypress); - ## generate the mocha + Mocha globals - ## on the specWindow, and get the new - ## _mocha instance - _mocha = globals(specWindow, reporter) + //# generate the mocha + Mocha globals + //# on the specWindow, and get the new + //# _mocha instance + const _mocha = globals(specWindow, reporter); - _runner = getRunner(_mocha) + const _runner = getRunner(_mocha); return { - _mocha + _mocha, - createRootTest: (title, fn) -> - r = new Test(title, fn) - _runner.suite.addTest(r) - r + createRootTest(title, fn) { + const r = new Test(title, fn); + _runner.suite.addTest(r); + return r; + }, - getRunner: -> - _runner + getRunner() { + return _runner; + }, - getRootSuite: -> - _mocha.suite + getRootSuite() { + return _mocha.suite; + }, - options: (runner) -> - runner.options(_mocha.options) - } + options(runner) { + return runner.options(_mocha.options); + } + }; +}; module.exports = { - restore + restore, - globals + globals, create -} +}; From edbcbee4ac4302b5ec63b7d50ba6f6d51bdaeab7 Mon Sep 17 00:00:00 2001 From: decaffeinate <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:30:03 -0500 Subject: [PATCH 007/300] decaffeinate: Run post-processing cleanups on mocha.coffee --- packages/driver/src/cypress/mocha.js | 276 +++++++++++++++------------ 1 file changed, 150 insertions(+), 126 deletions(-) diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index 5e718c55646b..8397c9ab4a92 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -1,230 +1,254 @@ +/* eslint-disable + brace-style, + no-unused-vars, + prefer-rest-params, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const _ = require("lodash"); -const $utils = require("./utils"); +const _ = require('lodash') +const $utils = require('./utils') //# in the browser mocha is coming back //# as window -const mocha = require("mocha"); +const mocha = require('mocha') -const Mocha = mocha.Mocha != null ? mocha.Mocha : mocha; -const { Test } = Mocha; -const { Runner } = Mocha; -const { Runnable } = Mocha; +const Mocha = mocha.Mocha != null ? mocha.Mocha : mocha +const { Test } = Mocha +const { Runner } = Mocha +const { Runnable } = Mocha -const runnerRun = Runner.prototype.run; -const runnerFail = Runner.prototype.fail; -const runnableRun = Runnable.prototype.run; -const runnableClearTimeout = Runnable.prototype.clearTimeout; -const runnableResetTimeout = Runnable.prototype.resetTimeout; +const runnerRun = Runner.prototype.run +const runnerFail = Runner.prototype.fail +const runnableRun = Runnable.prototype.run +const runnableClearTimeout = Runnable.prototype.clearTimeout +const runnableResetTimeout = Runnable.prototype.resetTimeout //# don't let mocha polute the global namespace -delete window.mocha; -delete window.Mocha; +delete window.mocha +delete window.Mocha -const ui = function(specWindow, _mocha) { +const ui = function (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]; + _mocha.ui = function (name) { + this._ui = Mocha.interfaces[name] if (!this._ui) { - $utils.throwErrByPath("mocha.invalid_interface", { args: { name } }); + $utils.throwErrByPath('mocha.invalid_interface', { args: { name } }) } - this._ui = this._ui(this.suite); + 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); + this.suite.emit('pre-require', specWindow, null, this) - return this; - }; + return this + } - return _mocha.ui("bdd"); -}; + return _mocha.ui('bdd') +} -const set = function(specWindow, _mocha) { +const set = function (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); + const M = (specWindow.Mocha = Mocha) + const m = (specWindow.mocha = _mocha) //# also attach the Mocha class //# to the mocha instance for clarity - m.Mocha = M; + 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); -}; + return ui(specWindow, _mocha) +} -const globals = function(specWindow, reporter) { - if (reporter == null) { reporter = function() {}; } +const globals = function (specWindow, reporter) { + if (reporter == null) { + reporter = function () {} + } const _mocha = new Mocha({ reporter, - enableTimeouts: false - }); + enableTimeouts: false, + }) //# set mocha props on the specWindow - set(specWindow, _mocha); + set(specWindow, _mocha) //# return the newly created mocha instance - return _mocha; -}; + return _mocha +} -const getRunner = function(_mocha) { - Runner.prototype.run = function() { +const getRunner = function (_mocha) { + Runner.prototype.run = function () { //# reset our runner#run function //# so the next time we call it //# its normal again! - restoreRunnerRun(); + restoreRunnerRun() //# return the runner instance - return this; - }; + return this + } - return _mocha.run(); -}; + return _mocha.run() +} -const restoreRunnableClearTimeout = () => Runnable.prototype.clearTimeout = runnableClearTimeout; +const restoreRunnableClearTimeout = () => { + return Runnable.prototype.clearTimeout = runnableClearTimeout +} -const restoreRunnableResetTimeout = () => Runnable.prototype.resetTimeout = runnableResetTimeout; +const restoreRunnableResetTimeout = () => { + return Runnable.prototype.resetTimeout = runnableResetTimeout +} -var restoreRunnerRun = () => Runner.prototype.run = runnerRun; +const restoreRunnerRun = () => { + return Runner.prototype.run = runnerRun +} -const restoreRunnerFail = () => Runner.prototype.fail = runnerFail; +const restoreRunnerFail = () => { + return Runner.prototype.fail = runnerFail +} -const restoreRunnableRun = () => Runnable.prototype.run = runnableRun; +const restoreRunnableRun = () => { + return Runnable.prototype.run = runnableRun +} const patchRunnerFail = () => - //# matching the current Runner.prototype.fail except - //# changing the logic for determing whether this is a valid err - Runner.prototype.fail = function(runnable, err) { - //# 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); +//# matching the current Runner.prototype.fail except +//# changing the logic for determing whether this is a valid err +{ + return Runner.prototype.fail = function (runnable, err) { + //# 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; + ++this.failures - runnable.state = "failed"; + runnable.state = 'failed' - return this.emit("fail", runnable, err); + return this.emit('fail', runnable, err) } -; +} -const patchRunnableRun = Cypress => - Runnable.prototype.run = function(...args) { - const runnable = this; +const patchRunnableRun = (Cypress) => { + return Runnable.prototype.run = function (...args) { + const runnable = this - return Cypress.action("mocha:runnable:run", runnableRun, runnable, args); + return Cypress.action('mocha:runnable:run', runnableRun, runnable, args) } -; +} -const patchRunnableClearTimeout = () => - Runnable.prototype.clearTimeout = function() { - //# call the original - runnableClearTimeout.apply(this, arguments); +const patchRunnableClearTimeout = () => { + return Runnable.prototype.clearTimeout = function () { + //# call the original + runnableClearTimeout.apply(this, arguments) - //# nuke the timer property - //# for testing purposes - return this.timer = null; + this.timer = null } -; +} -const patchRunnableResetTimeout = () => - Runnable.prototype.resetTimeout = function() { - const runnable = this; +const patchRunnableResetTimeout = () => { + return Runnable.prototype.resetTimeout = function () { + const runnable = this - const ms = this.timeout() || 1e9; + const ms = this.timeout() || 1e9 - this.clearTimeout(); + this.clearTimeout() - const getErrPath = function() { + const getErrPath = function () { //# we've yield an explicit done callback if (runnable.async) { - return "mocha.async_timed_out"; - } else { - //# 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"; + return 'mocha.async_timed_out' } - }; - return this.timer = setTimeout(function() { - const errMessage = $utils.errMessageByPath(getErrPath(), { ms }); - runnable.callback(new Error(errMessage)); - return runnable.timedOut = true; + //# 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 errMessage = $utils.errMessageByPath(getErrPath(), { ms }) + + runnable.callback(new Error(errMessage)) + runnable.timedOut = true } - , ms); + , ms) } -; +} -const restore = function() { - restoreRunnerRun(); - restoreRunnerFail(); - restoreRunnableRun(); - restoreRunnableClearTimeout(); - return restoreRunnableResetTimeout(); -}; +const restore = function () { + restoreRunnerRun() + restoreRunnerFail() + restoreRunnableRun() + restoreRunnableClearTimeout() -const override = function(Cypress) { - patchRunnerFail(); - patchRunnableRun(Cypress); - patchRunnableClearTimeout(); - return patchRunnableResetTimeout(); -}; + return restoreRunnableResetTimeout() +} -const create = function(specWindow, Cypress, reporter) { - restore(); +const override = function (Cypress) { + patchRunnerFail() + patchRunnableRun(Cypress) + patchRunnableClearTimeout() - override(Cypress); + return patchRunnableResetTimeout() +} + +const create = function (specWindow, Cypress, reporter) { + restore() + + override(Cypress) //# generate the mocha + Mocha globals //# on the specWindow, and get the new //# _mocha instance - const _mocha = globals(specWindow, reporter); + const _mocha = globals(specWindow, reporter) - const _runner = getRunner(_mocha); + const _runner = getRunner(_mocha) return { _mocha, - createRootTest(title, fn) { - const r = new Test(title, fn); - _runner.suite.addTest(r); - return r; + createRootTest (title, fn) { + const r = new Test(title, fn) + + _runner.suite.addTest(r) + + return r }, - getRunner() { - return _runner; + getRunner () { + return _runner }, - getRootSuite() { - return _mocha.suite; + getRootSuite () { + return _mocha.suite }, - options(runner) { - return runner.options(_mocha.options); - } - }; -}; + options (runner) { + return runner.options(_mocha.options) + }, + } +} module.exports = { restore, globals, - create -}; + create, +} From 7b69aa7cb43ceb36dd0e1cd69a31095e3fad778f Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:24:01 -0500 Subject: [PATCH 008/300] cleanup decaf --- packages/driver/src/cypress/mocha.js | 21 +---- packages/driver/src/cypress/runner.js | 117 +++++++++++++------------- 2 files changed, 62 insertions(+), 76 deletions(-) diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index 8397c9ab4a92..e011c2b40b68 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -1,17 +1,3 @@ -/* eslint-disable - brace-style, - no-unused-vars, - prefer-rest-params, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const _ = require('lodash') const $utils = require('./utils') //# in the browser mocha is coming back @@ -122,10 +108,9 @@ const restoreRunnableRun = () => { return Runnable.prototype.run = runnableRun } -const patchRunnerFail = () => //# matching the current Runner.prototype.fail except //# changing the logic for determing whether this is a valid err -{ +const patchRunnerFail = () => { return Runner.prototype.fail = function (runnable, err) { //# if this isnt a correct error object then just bail //# and call the original function @@ -151,9 +136,9 @@ const patchRunnableRun = (Cypress) => { } const patchRunnableClearTimeout = () => { - return Runnable.prototype.clearTimeout = function () { + return Runnable.prototype.clearTimeout = function (...args) { //# call the original - runnableClearTimeout.apply(this, arguments) + runnableClearTimeout.apply(this, args) this.timer = null } diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index 12978940a63a..ef5bcf64ecf6 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -1,17 +1,3 @@ -/* eslint-disable - brace-style, - default-case, - no-case-declarations, - no-cond-assign, - no-const-assign, - no-undef, - no-unused-vars, - no-var, - one-var, - prefer-rest-params, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns @@ -186,12 +172,11 @@ const reduceProps = (obj, props) => { , {}) } -const wrap = (runnable) => //# we need to optimize wrap by converting //# tests to an id-based object which prevents //# us from recursively iterating through every //# parent since we could just return the found test -{ +const wrap = (runnable) => { return reduceProps(runnable, RUNNABLE_PROPS) } @@ -220,7 +205,6 @@ const forceGc = function (obj) { //# references to ctx, and removes callback //# functions for closures for (let key of Object.keys(obj.ctx || {})) { - const value = obj.ctx[key] obj.ctx[key] = undefined } @@ -265,8 +249,10 @@ const onFirstTest = function (suite, fn) { } } - for (suite of suite.suites) { - if (test = onFirstTest(suite, fn)) { + for (const suite of suite.suites) { + const test = onFirstTest(suite, fn) + + if (test) { return test } } @@ -290,14 +276,16 @@ const getAllSiblingTests = function (suite, getTestById) { const getTestFromHook = function (hook, suite, getTestById) { //# if theres already a currentTest use that - let found, test + const test = hook != null ? hook.ctx.currentTest : undefined - if (test = hook != null ? hook.ctx.currentTest : undefined) { + if (test) { return test } //# if we have a hook id then attempt //# to find the test by its id + let found + if (hook != null ? hook.id : undefined) { found = onFirstTest(suite, (test) => { return hook.id === test.id @@ -323,7 +311,7 @@ const getTestFromHook = function (hook, suite, getTestById) { //# have one last final fallback where //# we just return true on the very first //# test (used in testing) - return onFirstTest(suite, (test) => { + return onFirstTest(suite, () => { return true }) } @@ -388,11 +376,11 @@ const overrideRunnerHook = function (Cypress, _runner, getTestById, getTest, set const _runnerHook = _runner.hook _runner.hook = function (name, fn) { - const hooks = this.suite[`_${name}`] + // const hooks = this.suite[`_${name}`] const allTests = getTests() - const changeFnToRunAfterHooks = function () { + const changeFnToRunAfterHooks = function (...args) { const originalFn = fn const test = getTest() @@ -403,12 +391,12 @@ const overrideRunnerHook = function (Cypress, _runner, getTestById, getTest, set testAfterRun(test, Cypress) //# and now invoke next(err) - return originalFn(...arguments) + return originalFn(...args) } } switch (name) { - case 'afterEach': + case 'afterEach': { const t = getTest() //# find all of the grep'd _tests which share @@ -423,10 +411,14 @@ const overrideRunnerHook = function (Cypress, _runner, getTestById, getTest, set break - case 'afterAll': + } + + case 'afterAll': { //# find all of the grep'd allTests which share //# the same parent suite as our current _test - if (t = getTest()) { + const t = getTest() + + if (t) { const siblings = getAllSiblingTests(t.parent, getTestById) //# 1. if we're the very last test in the entire allTests @@ -447,6 +439,8 @@ const overrideRunnerHook = function (Cypress, _runner, getTestById, getTest, set break } + default: break + } return _runnerHook.call(this, name, fn) } @@ -483,7 +477,7 @@ const normalizeAll = function (suite, initialTests = {}, grep, setTestsById, set let hasTests = false //# only loop until we find the first test - onFirstTest(suite, (test) => { + onFirstTest(suite, () => { return hasTests = true }) @@ -517,7 +511,6 @@ const normalizeAll = function (suite, initialTests = {}, grep, setTestsById, set const normalize = function (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) { const normalizer = (runnable) => { - let i runnable.id = getTestId() @@ -532,7 +525,9 @@ const normalize = function (runnable, tests, initialTests, grep, grepIsDefault, //# if we have a runnable in the initial state //# then merge in existing properties into the runnable - if (i = initialTests[runnable.id]) { + const i = initialTests[runnable.id] + + if (i) { _.each(RUNNABLE_LOGS, (type) => { return _.each(i[type], onLogsById) }) @@ -613,14 +608,14 @@ const normalize = function (runnable, tests, initialTests, grep, grepIsDefault, return obj } -const afterEachFailed = function (Cypress, test, err) { - test.state = 'failed' - test.err = wrapErr(err) +// const afterEachFailed = function (Cypress, test, err) { +// test.state = 'failed' +// test.err = wrapErr(err) - return Cypress.action('runner:test:end', wrap(test)) -} +// return Cypress.action('runner:test:end', wrap(test)) +// } -const hookFailed = function (hook, err, hookName, getTestById, getTest) { +const hookFailed = function (hook, err, hookName, getTestById, getTest, Cypress) { //# finds the test by returning the first test from //# the parent or looping through the suites until //# it finds the first test @@ -819,7 +814,7 @@ const _runnerListeners = function (_runner, Cypress, _emissions, getTestById, ge //# if a hook fails (such as a before) then the test will never //# get run and we'll need to make sure we set the test so that //# the TEST_AFTER_RUN_EVENT fires correctly - return hookFailed(runnable, runnable.err, hookName, getTestById, getTest) + return hookFailed(runnable, runnable.err, hookName, getTestById, getTest, Cypress) } }) } @@ -833,8 +828,8 @@ const create = function (specWindow, mocha, Cypress, cy) { _runner.suite = mocha.getRootSuite() - specWindow.onerror = function () { - let err = cy.onSpecWindowUncaughtException(...arguments) + specWindow.onerror = function (...args) { + let err = cy.onSpecWindowUncaughtException(...args) //# err will not be returned if cy can associate this //# uncaught exception to an existing runnable @@ -889,9 +884,8 @@ const create = function (specWindow, mocha, Cypress, cy) { } let _startTime = null - const getTestId = () => //# increment the id counter - { + const getTestId = () => { return `r${_id += 1}` } @@ -955,9 +949,9 @@ const create = function (specWindow, mocha, Cypress, cy) { //# need to handle //# ignoreLeaks, asyncOnly, globals - let re + const re = options.grep - if (re = options.grep) { + if (re) { return this.grep(re) } }, @@ -1010,12 +1004,13 @@ const create = function (specWindow, mocha, Cypress, cy) { }, onRunnableRun (runnableRun, runnable, args) { - let lifecycleStart, test if (!runnable.id) { throw new Error('runnable must have an id', runnable.id) } + let test + switch (runnable.type) { case 'hook': test = getTest() || getTestFromHook(runnable, runnable.parent, getTestById) @@ -1024,6 +1019,8 @@ const create = function (specWindow, mocha, Cypress, cy) { case 'test': test = runnable break + + default: break } //# closure for calculating the actual @@ -1035,6 +1032,7 @@ const create = function (specWindow, mocha, Cypress, cy) { let fnDurationEnd = null let afterFnDurationStart = null let afterFnDurationEnd = null + let lifecycleStart //# when this is a hook, capture the real start //# date so we can calculate our test's duration @@ -1096,6 +1094,7 @@ const create = function (specWindow, mocha, Cypress, cy) { afterFnDuration: afterFnDurationEnd - afterFnDurationStart, }) break + default: break } return _next(err) @@ -1196,7 +1195,7 @@ const create = function (specWindow, mocha, Cypress, cy) { }, countByTestState (tests, state) { - const count = _.filter(tests, (test, key) => { + const count = _.filter(tests, (test) => { return test.state === state }) @@ -1223,16 +1222,16 @@ const create = function (specWindow, mocha, Cypress, cy) { //# search through all of the tests //# until we find the current test //# and break then - for (var test of _tests) { + for (let test of _tests) { if (test.id === id) { break } else { test = wrapAll(test) _.each(RUNNABLE_LOGS, (type) => { - let logs + const logs = test[type] - if (logs = test[type]) { + if (logs) { test[type] = _.map(logs, $Log.toSerializedJSON) } }) @@ -1268,25 +1267,25 @@ const create = function (specWindow, mocha, Cypress, cy) { getDisplayPropsForLog: $Log.getDisplayProps, getConsolePropsForLogById (logId) { - let attrs + const attrs = _logsById[logId] - if (attrs = _logsById[logId]) { + if (attrs) { return $Log.getConsoleProps(attrs) } }, getSnapshotPropsForLogById (logId) { - let attrs + const attrs = _logsById[logId] - if (attrs = _logsById[logId]) { + if (attrs) { return $Log.getSnapshotProps(attrs) } }, getErrorByTestId (testId) { - let test + const test = getTestById(testId) - if (test = getTestById(testId)) { + if (test) { return wrapErr(test.err) } }, @@ -1336,7 +1335,6 @@ const create = function (specWindow, mocha, Cypress, cy) { //# we dont need to hold a log reference //# to anything in memory when we're headless //# because you cannot inspect any logs - let existing if (!isInteractive) { return @@ -1357,7 +1355,9 @@ const create = function (specWindow, mocha, Cypress, cy) { _testsQueue.push(test) } - if (existing = _logsById[attrs.id]) { + const existing = _logsById[attrs.id] + + if (existing) { //# because log:state:changed may //# fire at a later time, its possible //# we've already cleaned up these attrs @@ -1375,7 +1375,8 @@ const create = function (specWindow, mocha, Cypress, cy) { const { testId, instrument } = attrs - if (test = getTestById(testId)) { + test = getTestById(testId) + if (test) { //# pluralize the instrument //# as a property on the runnable let name From 06ec5ce4e5dfb73ee524d8f5d68b7f8bf2b481bc Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:26:26 -0500 Subject: [PATCH 009/300] add retry logic --- packages/driver/src/cypress/mocha.js | 17 ++ packages/driver/src/cypress/runner.js | 64 ++++- .../integration/cypress/retries_spec.js | 266 ++++++++++++++++++ 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 packages/driver/test/cypress/integration/cypress/retries_spec.js diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index e011c2b40b68..62ac2b527820 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -9,6 +9,7 @@ const { Test } = Mocha const { Runner } = Mocha const { Runnable } = Mocha +const testClone = Test.prototype.clone const runnerRun = Runner.prototype.run const runnerFail = Runner.prototype.fail const runnableRun = Runnable.prototype.run @@ -108,6 +109,21 @@ const restoreRunnableRun = () => { return Runnable.prototype.run = runnableRun } +const patchTestClone = () => { + return Test.prototype.clone = function (...args) { + if (this.trueFn) { + this.fn = this.trueFn + } + + const ret = testClone.apply(this, args) + + ret.id = this.id + ret.err = null + + return ret + } +} + //# matching the current Runner.prototype.fail except //# changing the logic for determing whether this is a valid err const patchRunnerFail = () => { @@ -189,6 +205,7 @@ const override = function (Cypress) { patchRunnerFail() patchRunnableRun(Cypress) patchRunnableClearTimeout() + patchTestClone() return patchRunnableResetTimeout() } diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index ef5bcf64ecf6..d96a815fa68a 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -438,7 +438,7 @@ const overrideRunnerHook = function (Cypress, _runner, getTestById, getTest, set } break - } + } default: break } @@ -932,6 +932,45 @@ const create = function (specWindow, mocha, Cypress, cy) { overrideRunnerHook(Cypress, _runner, getTestById, getTest, setTest, getTests) + const getDefaultRetries = () => { + return Cypress.env('RETRIES') + } + + const onNextError = (runnable, next, err) => { + + const r = runnable + const isHook = r.type === 'hook' + const test = r.ctx.currentTest || r + const isBeforeHook = isHook && r.hookName.match(/before/) + + const fail = function () { + return next.call(this, err) + } + const noFail = function () { + test.err = null + + return next.call(this) + } + + if (err) { + if (test._retries === -1) { + test._retries = getDefaultRetries() + } + + if (isBeforeHook && test._currentRetry < test._retries) { + test.trueFn = test.fn + test.fn = function () { + throw err + } + + return noFail() + } + } + + return fail() + + } + return { grep (re) { if (arguments.length) { @@ -1059,10 +1098,29 @@ const create = function (specWindow, mocha, Cypress, cy) { fire(TEST_BEFORE_RUN_EVENT, test, Cypress) } + const isHook = runnable.type === 'hook' + const isAfterAllHook = isHook && runnable.hookName.match(/after all/) + //# extract out the next(fn) which mocha uses to //# move to the next runnable - this will be our async seam + // const _next = args[0] const _next = args[0] + if (isAfterAllHook) { + if (test.state !== 'failed') { + test.err = null + test.state = 'passed' + } + } + + if ( + isHook && + test.trueFn && + !isAfterAllHook + ) { + return _next.call(this) + } + const next = function (err) { //# now set the duration of the after runnable run async event afterFnDurationEnd = (wallClockEnd = new Date()) @@ -1097,6 +1155,10 @@ const create = function (specWindow, mocha, Cypress, cy) { default: break } + if (err) { + return onNextError(runnable, _next, err) + } + return _next(err) } diff --git a/packages/driver/test/cypress/integration/cypress/retries_spec.js b/packages/driver/test/cypress/integration/cypress/retries_spec.js new file mode 100644 index 000000000000..e2cea8500bd5 --- /dev/null +++ b/packages/driver/test/cypress/integration/cypress/retries_spec.js @@ -0,0 +1,266 @@ +/// + +Cypress.config('defaultCommandTimeout', 100) +Cypress.env('RETRIES', 0) + +Object.defineProperty(Cypress, 'currentTest', { + get () { + const r = cy.state('runnable') + + if (!r) { + const err = new Error() + + err.message = 'Cypress.currentTest cannot be accessed outside a test or hook (it, before, after, beforeEach, afterEach)' + throw err + } + + return r && r.ctx.currentTest || r + }, +}) + +describe('Cypress.currentTest', () => { + let error + let lastId + + try { + Cypress.currentTest + } catch (e) { + error = e + } + + it('throws error outside spec', function () { + expect(error).ok.property('message').contains('currentTest') + expect(this._runnable).eq(Cypress.currentTest) + expect(lastId).eq(this._runnable.id) + }) + + beforeEach(() => { + lastId = Cypress.currentTest.id + }) + // Cypress.log({message:'set'}) + it('gets correct runnable', () => { + expect(lastId).eq(Cypress.currentTest.id) + }) +}) + +describe('deeply nested', () => { + Cypress.config('defaultCommandTimeout', 100) + const pushHook = (name) => { + // console.log(`%c${name}`, 'color:blue') + expect(name).ok + hooks.push(name) + } + let hooks = [] + let foo = 0 + + const failUntil = (num) => { + if (num === true) { + expect(false).ok + } + + if (foo <= num) { + cy.log(Cypress._.cloneDeep(Cypress.config())) + cy.get(`failOn${foo}`, { timeout: 10 }) + } + } + + cy.on('fail', (err) => { + hooks.push('FAIL') + throw err + }) + + beforeEach(() => { + Cypress.currentTest.retries(4) + pushHook('BE 0') + foo++ + }) + describe('1', () => { + before(() => { + pushHook('B 1') + }) + beforeEach(() => { + pushHook('BE 1') + failUntil(1) + }) + + describe('2', () => { + beforeEach(() => { + pushHook('BE 2') + failUntil(2) + }) + beforeEach(() => { + pushHook('BE 2 B') + }) + beforeEach(() => { + pushHook('BE 2 C') + }) + it('T 2', () => { + pushHook('T 2') + failUntil(3) + expect(hooks).deep.eq([ + 'B 1', + 'BE 0', + 'BE 1', + 'BE 0', + 'BE 1', + 'BE 2', + 'BE 0', + 'BE 1', + 'BE 2', + 'BE 2 B', + 'BE 2 C', + 'T 2', + 'AE 2', + 'AE 1', + 'AE 1 B', + 'AE 0', + 'BE 0', + 'BE 1', + 'BE 2', + 'BE 2 B', + 'BE 2 C', + 'T 2', + ]) + hooks = [] + }) + + it('T 2 B', () => { + pushHook('T 2 B') + failUntil(4) + }) + afterEach(function () { + pushHook('AE 2') + }) + }) + afterEach(function () { + pushHook('AE 1') + }) + afterEach(function () { + pushHook('AE 1 B') + }) + }) + afterEach(() => { + pushHook('AE 0') + }) + after(() => { + pushHook('AA 0') + expect(hooks).deep.eq([ + 'AE 2', + 'AE 1', + 'AE 1 B', + 'AE 0', + 'BE 0', + 'BE 1', + 'BE 2', + 'BE 2 B', + 'BE 2 C', + 'T 2 B', + 'AE 2', + 'AE 1', + 'AE 1 B', + 'AE 0', + 'AA 0', + ]) + }) +}) + +describe('async', () => { + it('pass using done', function (done) { + cy.on('fail', () => { + done() + }) + expect(false).ok + }) +}) + +describe('fail 10 times', function () { + this.retries(10) + + let foo = 0 + + beforeEach(() => { + foo++ + expect(foo).gt(2) + }) + it('test', function () { + expect(foo).gt(10) + expect(this._runnable.currentRetry()).eq(10) + }) +}) + +describe('retries set via Cypress.currentTest.retries', () => { + let logs = [] + + beforeEach(() => { + Cypress.env('RETRIES', 0) + if (!Cypress.currentTest._currentRetry) { + logs = [] + } + + cy.on('log:added', (attr) => { + logs.push(attr) + }) + Cypress.currentTest.retries(1) + }) + + it('can set retries in hooks', () => { + expect(logs.length).eq(1) + }) + + it('prefers test retries over env var', () => { + Cypress.currentTest.retries(2) + + expect(logs.length, 'log length').eq(2) + cy.log('foo') + }) +}) + +describe('after all hook', () => { + describe('after hook fails test', () => { + let didFail = false + + describe('should fail in after hook', () => { + it('fails but caught', () => { + expect(true).ok + }) + after(() => { + cy.on('fail', () => { + didFail = true + // Cypress.log({message: didFail}) + // throw err + }) + expect(false).ok + }) + }) + describe('verify prev fail', () => { + it('did fail', () => { + expect(didFail).ok + }) + }) + }) + describe('after hook lets test fail', () => { + let didFail = false + let didRunAfterHook = false + + describe('should fail in after hook', () => { + it('fails but caught', () => { + cy.on('fail', () => { + didFail = true + // Cypress.log({message: didFail}) + // throw err + }) + expect(false).ok + }) + after(() => { + didRunAfterHook = true + expect(true).ok + }) + }) + describe('verify prev fail', () => { + it('did fail', () => { + expect(didFail).ok + expect(didRunAfterHook).ok + }) + }) + }) +}) From 09bfc2afa498a5ae578fe02cda5a95e5599f43b2 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 6 Mar 2019 10:52:21 +0630 Subject: [PATCH 010/300] Update styles to contain 'Attempt' for test retries --- packages/reporter/src/attempts/attempts.jsx | 2 +- packages/reporter/src/attempts/attempts.scss | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/reporter/src/attempts/attempts.jsx b/packages/reporter/src/attempts/attempts.jsx index 5eb9b6d586db..1e748fa69fb6 100644 --- a/packages/reporter/src/attempts/attempts.jsx +++ b/packages/reporter/src/attempts/attempts.jsx @@ -16,7 +16,7 @@ const NoCommands = observer(() => ( const AttemptHeader = ({ attempt, retriesCount }) => ( - {attempt.id}/{retriesCount} + Attempt {attempt.id} diff --git a/packages/reporter/src/attempts/attempts.scss b/packages/reporter/src/attempts/attempts.scss index 98c58f5554ab..13f018ad6a1d 100644 --- a/packages/reporter/src/attempts/attempts.scss +++ b/packages/reporter/src/attempts/attempts.scss @@ -1,24 +1,22 @@ .reporter .runnable.suite .attempt-name { - border-top: 1px solid #bbb; + border-top: 1px solid #dcdcdc; margin: 20px 0 30px; height: 0; position: relative; .attempt-tag { - border: 1px solid #bbb; + border: 1px solid #d5d5d5; border-radius: 10px; - padding: 2px 1px; + box-shadow: 0 1px 1px 0 rgba(0,0,0,0.20); + padding: 2px 5px; background-color: #fff; position: absolute; top: -12px; right: 10px; + font-size: 11px; &:hover { - background-color: #bbb; - color: #fff; - i { - color: #fff; - } + background-color: #e8e8e8; } } @@ -29,6 +27,7 @@ .hook-failed-message { i { color: $fail; + font-size: 13px; } } } \ No newline at end of file From 801dce278bab77f4add85ca341f30e0c57c23881 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Wed, 6 Mar 2019 09:37:30 -0500 Subject: [PATCH 011/300] add reporter cypress test necessities --- circle.yml | 3 +++ packages/reporter/cypress.json | 1 + packages/reporter/package.json | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index baaa3c8842aa..2ba21ceb24b2 100644 --- a/circle.yml +++ b/circle.yml @@ -739,6 +739,9 @@ linux-workflow: &linux-workflow - desktop-gui-integration-tests-2x: requires: - build + - reporter-integration-tests: + requires: + - build - run-launcher: requires: - build diff --git a/packages/reporter/cypress.json b/packages/reporter/cypress.json index b16929a9f710..40950ebabc22 100644 --- a/packages/reporter/cypress.json +++ b/packages/reporter/cypress.json @@ -1,4 +1,5 @@ { + "projectId": "ypt4pf", "viewportWidth": 400, "viewportHeight": 450, "supportFile": false, diff --git a/packages/reporter/package.json b/packages/reporter/package.json index 5a56680789a3..82e0f8a815c6 100644 --- a/packages/reporter/package.json +++ b/packages/reporter/package.json @@ -19,7 +19,9 @@ "clean-deps": "rm -rf node_modules", "pretest": "npm run check-deps-pre", "test": "node ./scripts/test.js", - "lint": "bin-up eslint --fix lib/*.js scripts/*.js src/*.js* src/**/*.js*" + "lint": "bin-up eslint --fix lib/*.js scripts/*.js src/*.js* src/**/*.js*", + "cypress:open": "node ../../scripts/cypress open --project .", + "cypress:run": "node ../../scripts/cypress run --project ." }, "files": [ "dist" From 79cdb7bd1f71e431fa2f153314d418651bc50998 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Thu, 7 Mar 2019 11:12:28 -0500 Subject: [PATCH 012/300] update css to match latest design --- packages/reporter/src/attempts/attempts.jsx | 25 ++++-- packages/reporter/src/attempts/attempts.scss | 94 ++++++++++++++------ packages/reporter/src/commands/commands.scss | 4 +- packages/reporter/src/main.scss | 2 +- packages/reporter/src/test/test.jsx | 11 +-- 5 files changed, 88 insertions(+), 48 deletions(-) diff --git a/packages/reporter/src/attempts/attempts.jsx b/packages/reporter/src/attempts/attempts.jsx index 1e748fa69fb6..a5aba44d4521 100644 --- a/packages/reporter/src/attempts/attempts.jsx +++ b/packages/reporter/src/attempts/attempts.jsx @@ -2,8 +2,11 @@ import cs from 'classnames' import _ from 'lodash' import { observer } from 'mobx-react' import React from 'react' + +import Agents from '../agents/agents' import Collapsible from '../collapsible/collapsible' import Hooks from '../hooks/hooks' +import Routes from '../routes/routes' const NoCommands = observer(() => (
      @@ -13,33 +16,37 @@ const NoCommands = observer(() => (
    )) -const AttemptHeader = ({ attempt, retriesCount }) => ( +const AttemptHeader = ({ index }) => ( - Attempt {attempt.id} - + Attempt {index + 1} + ) -const Attempt = observer(({ model, attempt, retriesCount }) => ( +const Attempt = observer(({ model, index }) => (
  • } + header={} headerClass='attempt-name' isOpen={true} > -
      + + +
      {model.commands.length ? : } -
    +
  • )) -const Attempts = observer(({ model, attempts, retriesCount }) => ( +const Attempts = observer(({ model, attempts }) => (
      - {_.map(attempts, (attempt) => )} + {_.map(attempts, (attempt, index) => { + return + })}
    )) diff --git a/packages/reporter/src/attempts/attempts.scss b/packages/reporter/src/attempts/attempts.scss index 13f018ad6a1d..512bcd90f13e 100644 --- a/packages/reporter/src/attempts/attempts.scss +++ b/packages/reporter/src/attempts/attempts.scss @@ -1,33 +1,75 @@ -.reporter .runnable.suite .attempt-name { - border-top: 1px solid #dcdcdc; - margin: 20px 0 30px; - height: 0; - position: relative; +.reporter { + .attempt-item { + margin-bottom: 7px; + + > .collapsible { + position: relative; - .attempt-tag { - border: 1px solid #d5d5d5; - border-radius: 10px; - box-shadow: 0 1px 1px 0 rgba(0,0,0,0.20); - padding: 2px 5px; - background-color: #fff; - position: absolute; - top: -12px; - right: 10px; - font-size: 11px; - - &:hover { - background-color: #e8e8e8; + &:before { + border-left: 1px solid #dcdcdc; + content: ''; + left: 5px; + position: absolute; + top: 22px; + height: 15px; + } + + &.is-open:before { + display: none; + } } } - .collapsible-indicator { - display: none; - } + .attempt-name { + display: flex; + justify-content: flex-end; + position: relative; + width: 100%; + + &:before { + border-top: 1px solid #dcdcdc; + content: ''; + left: 15px; + position: absolute; + right: 0; + top: 13px; + } + + &:after { + color: #a2a2a2; + content: '•'; + left: 3px; + position: absolute; + top: 6px; + } + + .attempt-tag { + border: 1px solid #d5d5d5; + border-radius: 7px; + box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.20); + font-size: 11px; + margin-right: 20px; + padding: 2px 5px; + position: relative; + background-color: #fff; + + &:hover { + background-color: #e8e8e8; + } + } + + .collapsible-indicator, + .collapsible-more { + display: none; + } + + .attempt-status-indicator { + margin-left: 3px; - .hook-failed-message { - i { - color: $fail; - font-size: 13px; + i { + color: $fail; + font-size: 13px; + } } } -} \ No newline at end of file +} diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 87cb9aa8756d..b5a1cb442904 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -1,7 +1,7 @@ .reporter { .hooks-container { - margin: 5px 0 5px 5px; - padding-left: 10px; + margin-left: 5px; + padding: 2px 0 2px 10px; border-left: 1px dotted #DDD; .hook-item { diff --git a/packages/reporter/src/main.scss b/packages/reporter/src/main.scss index 2c5b02c2c5a0..3f84c3bc0431 100644 --- a/packages/reporter/src/main.scss +++ b/packages/reporter/src/main.scss @@ -4,4 +4,4 @@ @import 'lib/fonts'; @import 'lib/base'; @import 'lib/tooltip'; -@import '!(lib)*/**/*'; +@import './!(lib)*/**/*'; diff --git a/packages/reporter/src/test/test.jsx b/packages/reporter/src/test/test.jsx index 021dc1b918e3..7880cdb17cef 100644 --- a/packages/reporter/src/test/test.jsx +++ b/packages/reporter/src/test/test.jsx @@ -11,8 +11,6 @@ import runnablesStore from '../runnables/runnables-store' import scroller from '../lib/scroller' import Attempts from '../attempts/attempts' -import Agents from '../agents/agents' -import Routes from '../routes/routes' import FlashOnClick from '../lib/flash-on-click' @observer @@ -87,13 +85,10 @@ class Test extends Component { const { model } = this.props - const retriesCount = 3 const attempts = [ { - id: 1, }, { - id: 2, }, ] @@ -104,11 +99,7 @@ class Test extends Component { e.stopPropagation() }} > - - -
    - {retriesCount ? : null} -
    + ) } From 9f6752a14b561eca923af9d2ddb092c07422d0a8 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Fri, 8 Mar 2019 15:15:24 -0500 Subject: [PATCH 013/300] =?UTF-8?q?don=E2=80=99t=20display=20line=20below?= =?UTF-8?q?=20last=20collapsed=20attempt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/reporter/src/attempts/attempts.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/reporter/src/attempts/attempts.scss b/packages/reporter/src/attempts/attempts.scss index 512bcd90f13e..e072b5f7c6d0 100644 --- a/packages/reporter/src/attempts/attempts.scss +++ b/packages/reporter/src/attempts/attempts.scss @@ -18,6 +18,10 @@ display: none; } } + + &:last-child > .collapsible:before { + display: none; + } } .attempt-name { From 6235acdc1f7ec10028d0b28a4e86516415a0d38c Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Mon, 11 Mar 2019 14:51:06 -0400 Subject: [PATCH 014/300] implement retries to work with runnables:ready --- packages/reporter/cypress.json | 2 +- packages/reporter/cypress/.eslintrc.json | 8 + .../cypress/fixtures/retries_runnables.json | 238 ++++++++++++++++++ .../cypress/integration/retries_spec.js | 50 ++++ packages/reporter/src/.eslintrc | 5 +- .../reporter/src/attempts/attempt-model.js | 118 +++++++++ packages/reporter/src/attempts/attempts.jsx | 88 +++++-- packages/reporter/src/attempts/attempts.scss | 58 ++++- .../reporter/src/collapsible/collapsible.jsx | 5 +- packages/reporter/src/lib/mixins.scss | 41 +++ packages/reporter/src/lib/variables.scss | 1 + packages/reporter/src/main-runner.scss | 1 + packages/reporter/src/main.scss | 1 + .../src/runnables/runnable-and-suite.jsx | 1 + .../reporter/src/runnables/runnables-store.js | 60 +---- .../reporter/src/runnables/runnables.scss | 50 ++-- packages/reporter/src/test/test-model.js | 77 ++++-- packages/reporter/src/test/test.jsx | 25 +- 18 files changed, 678 insertions(+), 151 deletions(-) create mode 100644 packages/reporter/cypress/.eslintrc.json create mode 100644 packages/reporter/cypress/fixtures/retries_runnables.json create mode 100644 packages/reporter/cypress/integration/retries_spec.js create mode 100644 packages/reporter/src/attempts/attempt-model.js create mode 100644 packages/reporter/src/lib/mixins.scss diff --git a/packages/reporter/cypress.json b/packages/reporter/cypress.json index 40950ebabc22..bb4635187cac 100644 --- a/packages/reporter/cypress.json +++ b/packages/reporter/cypress.json @@ -3,5 +3,5 @@ "viewportWidth": 400, "viewportHeight": 450, "supportFile": false, - "pluginFile": false + "pluginsFile": false } diff --git a/packages/reporter/cypress/.eslintrc.json b/packages/reporter/cypress/.eslintrc.json new file mode 100644 index 000000000000..5b988562725d --- /dev/null +++ b/packages/reporter/cypress/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "plugins": [ + "cypress" + ], + "env": { + "cypress/globals": true + } +} diff --git a/packages/reporter/cypress/fixtures/retries_runnables.json b/packages/reporter/cypress/fixtures/retries_runnables.json new file mode 100644 index 000000000000..ac8b3f52cdc8 --- /dev/null +++ b/packages/reporter/cypress/fixtures/retries_runnables.json @@ -0,0 +1,238 @@ +{ + "id": "r1", + "title": "", + "root": true, + "suites": [{ + "id": "r2", + "title": "suite 1", + "suites": [], + "tests": [ + { + "id": "r3", + "title": "no retries", + "state": "passed", + "attempts": [ + { + "attempt": 1, + "testId": "r3", + "state": "passed", + "commands": [ + { + "hookName": "before each", + "id": "c1", + "instrument": "command", + "message": "http://localhost:3000", + "name": "visit", + "state": "passed", + "testId": "r3", + "type": "parent" + } + ] + } + ] + }, + { + "id": "r4", + "title": "passed with retries", + "attempts": [ + { + "attempt": 1, + "testId": "r4", + "state": "failed", + "agents": [ + { + "callCount": 0, + "functionName": "btoa", + "hookName": "before each", + "id": "a1", + "instrument": "agent", + "name": "spy-1", + "testId": "r4", + "type": "spy-1" + } + ], + "commands": [ + { + "hookName": "before each", + "id": "c1", + "instrument": "command", + "message": "http://localhost:3000", + "name": "visit", + "state": "passed", + "testId": "r4", + "type": "parent" + }, + { + "hookName": "test", + "id": "c2", + "instrument": "command", + "message": ".foo", + "name": "get", + "state": "failed", + "testId": "r4", + "type": "parent" + } + ], + "routes": [ + { + "hookName": "before each", + "id": "ro1", + "instrument": "route", + "method": "GET", + "name": "route", + "testId": "r4", + "url": "/foo" + } + ] + }, + { + "attempt": 2, + "testId": "r4", + "state": "passed", + "agents": [ + { + "callCount": 0, + "functionName": "btoa", + "hookName": "before each", + "id": "a2", + "instrument": "agent", + "name": "spy-1", + "testId": "r4", + "type": "spy-1" + } + ], + "commands": [ + { + "hookName": "before each", + "id": "c3", + "instrument": "command", + "message": "http://localhost:3000", + "name": "visit", + "state": "passed", + "testId": "r4", + "type": "parent" + }, + { + "hookName": "test", + "id": "c4", + "instrument": "command", + "message": ".foo", + "name": "get", + "state": "passed", + "testId": "r4", + "type": "parent" + }, + { + "hookName": "test", + "id": "c5", + "instrument": "command", + "message": "expect **<.foo>** to have text **foo**", + "name": "assert", + "numElements": 1, + "renderProps": {}, + "state": "passed", + "testId": "r4", + "type": "child" + } + ], + "routes": [ + { + "hookName": "before each", + "id": "ro2", + "instrument": "route", + "method": "GET", + "name": "route", + "testId": "r4", + "url": "/foo" + } + ] + } + ] + } + ] + }, + { + "id": "r6", + "title": "suite 2", + "suites": [], + "tests": [ + { + "id": "r5", + "title": "failed with retries", + "attempts": [ + { + "attempt": 1, + "testId": "r5", + "state": "failed", + "agents": [], + "commands": [ + { + "hookName": "before each", + "id": "c1", + "instrument": "command", + "message": "http://localhost:3000", + "name": "visit", + "state": "passed", + "testId": "r5", + "type": "parent" + }, + { + "hookName": "test", + "id": "c2", + "instrument": "command", + "message": ".foo", + "name": "get", + "state": "failed", + "testId": "r5", + "type": "parent" + } + ], + "routes": [] + }, + { + "attempt": 2, + "testId": "r5", + "state": "failed", + "agents": [], + "commands": [ + { + "hookName": "before each", + "id": "c3", + "instrument": "command", + "message": "http://localhost:3000", + "name": "visit", + "state": "passed", + "testId": "r5", + "type": "parent" + }, + { + "hookName": "test", + "id": "c4", + "instrument": "command", + "message": ".foo", + "name": "get", + "state": "passed", + "testId": "r5", + "type": "parent" + }, + { + "hookName": "test", + "id": "c5", + "instrument": "command", + "message": "expected **<.foo>** to have text **bar**", + "name": "assert", + "numElements": 1, + "renderProps": {}, + "state": "failed", + "testId": "r5", + "type": "child" + } + ], + "routes": [] + } + ] + } + ] + }], + "tests": [] +} diff --git a/packages/reporter/cypress/integration/retries_spec.js b/packages/reporter/cypress/integration/retries_spec.js new file mode 100644 index 000000000000..5f2da5b66038 --- /dev/null +++ b/packages/reporter/cypress/integration/retries_spec.js @@ -0,0 +1,50 @@ +import { EventEmitter } from 'events' + +describe('retries', function () { + beforeEach(function () { + cy.fixture('retries_runnables').as('runnables') + + this.runner = new EventEmitter() + + cy.server() + cy.route('/foo') + + cy.visit('cypress/support/index.html').then((win) => { + cy.spy(win, 'btoa') + + win.render({ + runner: this.runner, + specPath: '/foo/bar', + }) + }) + + cy.get('.reporter').then(() => { + this.runner.emit('runnables:ready', this.runnables) + this.runner.emit('reporter:start', {}) + }) + }) + + it('does not shows attempts if there are no retries', () => { + cy + .contains('no retries') + .click() + .closest('.runnable-wrapper') + .contains('Attempt 1') + .should('not.be.visible') + }) + + // shows attempts after more than 1 + // shows yellow on side + // shows the right status indicators + // - active + // - processing + // - skipped? + // - pending? + // - failed + // - passed + // collapses properly + // shows routes/spies/hooks/commands + // errors in the right places + // - on each attempt + // - on the test itself +}) diff --git a/packages/reporter/src/.eslintrc b/packages/reporter/src/.eslintrc index 212723095723..14adb990123f 100644 --- a/packages/reporter/src/.eslintrc +++ b/packages/reporter/src/.eslintrc @@ -2,5 +2,8 @@ "extends": [ "plugin:cypress-dev/react", "plugin:cypress-dev/tests" - ] + ], + "globals": { + "test": false + } } diff --git a/packages/reporter/src/attempts/attempt-model.js b/packages/reporter/src/attempts/attempt-model.js new file mode 100644 index 000000000000..6f98034c337d --- /dev/null +++ b/packages/reporter/src/attempts/attempt-model.js @@ -0,0 +1,118 @@ +import _ from 'lodash' +import { computed, observable } from 'mobx' + +import Agent from '../agents/agent-model' +import Command from '../commands/command-model' +import Err from '../lib/err-model' +import Hook from '../hooks/hook-model' +import Route from '../routes/route-model' + +export default class Attempt { + @observable agents = [] + @observable attempts = [] + @observable commands = [] + @observable err = new Err({}) + @observable hooks = [] + // @observable isActive = null + // @observable isLongRunning = false + // @observable isOpen = false + @observable routes = [] + // @observable _state = null + + _logs = {} + + constructor (props) { + this._state = props.state + // this.err.update(props.err) + + _.each(props.agents, this.addLog) + _.each(props.commands, this.addLog) + _.each(props.routes, this.addLog) + } + + @computed get hasCommands () { + return !!this.commands.length + } + + @computed get isLongRunning () { + return _.some(this.commands, (command) => { + return command.isLongRunning + }) + } + + @computed get state () { + return this._state || (this.isActive ? 'active' : 'processing') + } + + addLog = (props) => { + switch (props.instrument) { + case 'command': { + this._addCommand(props) + break + } + case 'agent': { + this._addAgent(props) + break + } + case 'route': { + this._addRoute(props) + break + } + default: { + throw new Error(`Attempted to add log for unknown instrument: ${props.instrument}`) + } + } + } + + _addAgent (props) { + const agent = new Agent(props) + + this._logs[props.id] = agent + this.agents.push(agent) + } + + _addRoute (props) { + const route = new Route(props) + + this._logs[props.id] = route + this.routes.push(route) + } + + _addCommand (props) { + const command = new Command(props) + const hook = this._findOrCreateHook(props.hookName) + + this._logs[props.id] = command + this.commands.push(command) + hook.addCommand(command) + } + + updateLog (props) { + const log = this._logs[props.id] + + if (log) { + log.update(props) + } + } + + commandMatchingErr () { + return _(this.hooks) + .map((hook) => { + return hook.commandMatchingErr(this.err) + }) + .compact() + .last() + } + + _findOrCreateHook (name) { + const hook = _.find(this.hooks, { name }) + + if (hook) return hook + + const newHook = new Hook({ name }) + + this.hooks.push(newHook) + + return newHook + } +} diff --git a/packages/reporter/src/attempts/attempts.jsx b/packages/reporter/src/attempts/attempts.jsx index a5aba44d4521..806dc821a752 100644 --- a/packages/reporter/src/attempts/attempts.jsx +++ b/packages/reporter/src/attempts/attempts.jsx @@ -1,7 +1,8 @@ import cs from 'classnames' import _ from 'lodash' +import { action, observable } from 'mobx' import { observer } from 'mobx-react' -import React from 'react' +import React, { Component, Fragment } from 'react' import Agents from '../agents/agents' import Collapsible from '../collapsible/collapsible' @@ -16,36 +17,83 @@ const NoCommands = observer(() => ( )) -const AttemptHeader = ({ index }) => ( +const AttemptHeader = ({ index, isOpen }) => ( - - Attempt {index + 1} - - + + + + Attempt {index + 1} + ) -const Attempt = observer(({ model, index }) => ( -
  • - } - headerClass='attempt-name' - isOpen={true} - > +const AttemptContent = ({ isOpen, model }) => { + // performance optimization - don't render contents if not open + if (!isOpen) return null + + return ( +
    - {model.commands.length ? : } + {model.hasCommands ? : }
    -
    -
  • -)) + + ) +} + +@observer +class Attempt extends Component { + @observable isOpen = null + + render () { + const { index, model } = this.props + + return ( +
  • + } + headerClass='attempt-name' + isOpen={this._shouldBeOpen()} + onToggle={this._toggleOpen} + > + + +
  • + ) + } + + _shouldBeOpen () { + const { model, test } = this.props + + // if there are no retries, the 'Attempt #' header will not be + // shown and it will not be collapsible, so it should always be open + if (!test.hasMultipleAttempts) return true + + // if this.isOpen is non-null, prefer that since the user has + // explicity chosen to open or close the test + if (this.isOpen !== null) return this.isOpen + + // otherwise, look at reasons to auto-open the test + return model.isOpen + || model.isLongRunning + || test.isLastAttempt(model) + } + + @action _toggleOpen = () => { + if (this.isOpen === null) { + this.isOpen = !this._shouldBeOpen() + } else { + this.isOpen = !this.isOpen + } + } +} -const Attempts = observer(({ model, attempts }) => ( -