diff --git a/packages/driver/cypress/integration/commands/querying_spec.js b/packages/driver/cypress/integration/commands/querying_spec.js index 8ed6b39fda61..8bd65d3b2b87 100644 --- a/packages/driver/cypress/integration/commands/querying_spec.js +++ b/packages/driver/cypress/integration/commands/querying_spec.js @@ -502,20 +502,18 @@ describe('src/cy/commands/querying', () => { }) describe('.log', () => { - beforeEach(() => { - beforeEach(function () { - this.logs = [] - - cy.on('log:added', (attrs, log) => { - if (attrs.name === 'root') { - this.lastLog = log + beforeEach(function () { + this.logs = [] - this.logs.push(log) - } - }) + cy.on('log:added', (attrs, log) => { + if (attrs.name === 'root') { + this.lastLog = log - return null + this.logs.push(log) + } }) + + return null }) it('can turn off logging', () => { diff --git a/packages/driver/cypress/integration/cypress/runner_spec.js b/packages/driver/cypress/integration/cypress/runner_spec.js index ce581bf06ea2..6f1befff02ba 100644 --- a/packages/driver/cypress/integration/cypress/runner_spec.js +++ b/packages/driver/cypress/integration/cypress/runner_spec.js @@ -11,16 +11,6 @@ Cypress.on('test:after:run', (test) => { }) describe('src/cypress/runner', () => { - it('handles "double quotes" in test name', (done) => { - cy.once('log:added', (log) => { - expect(log.hookName).to.equal('test') - - return done() - }) - - return cy.wrap({}) - }) - context('pending tests', () => { it('is not pending', () => {}) diff --git a/packages/driver/src/cypress.js b/packages/driver/src/cypress.js index c17771339627..413aff81bcc1 100644 --- a/packages/driver/src/cypress.js +++ b/packages/driver/src/cypress.js @@ -198,7 +198,7 @@ class $Cypress { window.cy = this.cy this.isCy = this.cy.isCy this.log = $Log.create(this, this.cy, this.state, this.config) - this.mocha = $Mocha.create(specWindow, this) + this.mocha = $Mocha.create(specWindow, this, this.config) this.runner = $Runner.create(specWindow, this.mocha, this, this.cy) // wire up command create to cy diff --git a/packages/driver/src/cypress/cy.js b/packages/driver/src/cypress/cy.js index 77d28852bc1d..3cdbc2052d18 100644 --- a/packages/driver/src/cypress/cy.js +++ b/packages/driver/src/cypress/cy.js @@ -1270,7 +1270,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { return snapshots.getStyles(...args) }, - setRunnable (runnable, hookName) { + setRunnable (runnable, hookId) { // when we're setting a new runnable // prepare to run again! stopped = false @@ -1278,7 +1278,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) { // reset the promise again state('promise', undefined) - state('hookName', hookName) + state('hookId', hookId) state('runnable', runnable) diff --git a/packages/driver/src/cypress/log.js b/packages/driver/src/cypress/log.js index b2ae13f71f1b..363856aec9b1 100644 --- a/packages/driver/src/cypress/log.js +++ b/packages/driver/src/cypress/log.js @@ -13,7 +13,7 @@ const $errUtils = require('./error_utils') const groupsOrTableRe = /^(groups|table)$/ const parentOrChildRe = /parent|child/ const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight'.split(' ') -const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName hookName instrument isStubbed message method name numElements numResponses referencesAlias renderProps state testId timeout type url visible wallClockStartedAt'.split(' ') +const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName hookId instrument isStubbed message method name numElements numResponses referencesAlias renderProps state testId timeout type url visible wallClockStartedAt'.split(' ') const BLACKLIST_PROPS = 'snapshots'.split(' ') let delay = null @@ -172,7 +172,7 @@ const defaults = function (state, config, obj) { state: 'pending', instrument: 'command', url: state('url'), - hookName: state('hookName'), + hookId: state('hookId'), testId: runnable ? runnable.id : undefined, viewportWidth: state('viewportWidth'), viewportHeight: state('viewportHeight'), diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index 8e3231c62156..caf48df30dab 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -1,5 +1,6 @@ const _ = require('lodash') const $errUtils = require('./error_utils') +const $stackUtils = require('./stack_utils') // in the browser mocha is coming back // as window @@ -25,6 +26,7 @@ function invokeFnWithOriginalTitle (ctx, originalTitle, mochaArgs, fn) { return ret } + function overloadMochaFnForConfig (fnName, specWindow) { const _fn = specWindow[fnName] @@ -86,7 +88,53 @@ function overloadMochaFnForConfig (fnName, specWindow) { }) } -const ui = (specWindow, _mocha) => { +function getInvocationDetails (specWindow, config) { + if (specWindow.Error) { + let stack = (new specWindow.Error()).stack + + // firefox throws a different stack than chromium + // which includes this file (mocha.js) and mocha/.../common.js at the top + if (specWindow.Cypress.browser.family === 'firefox') { + stack = $stackUtils.stackWithLinesDroppedFromMarker(stack, 'mocha/lib/interfaces/common.js') + } + + return $stackUtils.getSourceDetailsForFirstLine(stack, config('projectRoot')) + } +} + +function overloadMochaHook (fnName, suite, specWindow, config) { + const _fn = suite[fnName] + + suite[fnName] = function (title, fn) { + const _createHook = this._createHook + + this._createHook = function (title, fn) { + const hook = _createHook.call(this, title, fn) + + hook.invocationDetails = getInvocationDetails(specWindow, config) + + return hook + } + + const ret = _fn.call(this, title, fn) + + this._createHook = _createHook + + return ret + } +} + +function overloadMochaTest (suite, specWindow, config) { + const _fn = suite.addTest + + suite.addTest = function (test) { + test.invocationDetails = getInvocationDetails(specWindow, config) + + return _fn.call(this, test) + } +} + +const ui = (specWindow, _mocha, config) => { // 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) { @@ -109,13 +157,21 @@ const ui = (specWindow, _mocha) => { overloadMochaFnForConfig('describe', specWindow) overloadMochaFnForConfig('context', specWindow) + // overload tests and hooks so that we can get the stack info + overloadMochaHook('beforeAll', this.suite.constructor.prototype, specWindow, config) + overloadMochaHook('beforeEach', this.suite.constructor.prototype, specWindow, config) + overloadMochaHook('afterAll', this.suite.constructor.prototype, specWindow, config) + overloadMochaHook('afterEach', this.suite.constructor.prototype, specWindow, config) + + overloadMochaTest(this.suite.constructor.prototype, specWindow, config) + return this } return _mocha.ui('bdd') } -const setMochaProps = (specWindow, _mocha) => { +const setMochaProps = (specWindow, _mocha, config) => { // 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 @@ -128,21 +184,17 @@ const setMochaProps = (specWindow, _mocha) => { // 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, config) } -const globals = (specWindow, reporter) => { - if (reporter == null) { - reporter = () => {} - } - +const createMocha = (specWindow, config) => { const _mocha = new Mocha({ - reporter, + reporter: () => {}, timeout: false, }) // set mocha props on the specWindow - setMochaProps(specWindow, _mocha) + setMochaProps(specWindow, _mocha, config) // return the newly created mocha instance return _mocha @@ -269,7 +321,7 @@ const override = (Cypress) => { patchRunnableResetTimeout() } -const create = (specWindow, Cypress, reporter) => { +const create = (specWindow, Cypress, config) => { restore() override(Cypress) @@ -277,7 +329,7 @@ const create = (specWindow, Cypress, reporter) => { // generate the mocha + Mocha globals // on the specWindow, and get the new // _mocha instance - const _mocha = globals(specWindow, reporter) + const _mocha = createMocha(specWindow, config) const _runner = getRunner(_mocha) @@ -307,7 +359,5 @@ const create = (specWindow, Cypress, reporter) => { module.exports = { restore, - globals, - create, } diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index fe7dcde7b3dd..fb3abd283f1c 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -17,8 +17,8 @@ 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 RUNNABLE_LOGS = 'routes agents commands'.split(' ') -const RUNNABLE_PROPS = 'id order title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings file originalTitle'.split(' ') +const RUNNABLE_LOGS = 'routes agents commands hooks'.split(' ') +const RUNNABLE_PROPS = 'id order title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings file originalTitle invocationDetails'.split(' ') const debug = require('debug')('cypress:driver:runner') @@ -131,6 +131,27 @@ const wrapAll = (runnable) => { ) } +const condenseHooks = (runnable, getHookId) => { + const hooks = _.compact(_.concat( + runnable._beforeAll, + runnable._beforeEach, + runnable._afterAll, + runnable._afterEach, + )) + + return _.map(hooks, (hook) => { + if (!hook.hookId) { + hook.hookId = getHookId() + } + + if (!hook.hookName) { + hook.hookName = getHookName(hook) + } + + return wrap(hook) + }) +} + const getHookName = (hook) => { // find the name of the hook by parsing its // title and pulling out whats between the quotes @@ -378,7 +399,7 @@ const hasOnly = (suite) => { ) } -const normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnable, onLogsById, getTestId) => { +const normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnable, onLogsById, getTestId, getHookId) => { let hasTests = false // only loop until we find the first test @@ -396,7 +417,7 @@ const normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnab // create optimized lookups for the tests without // traversing through it multiple times const tests = {} - const normalizedSuite = normalize(suite, tests, initialTests, onRunnable, onLogsById, getTestId) + const normalizedSuite = normalize(suite, tests, initialTests, onRunnable, onLogsById, getTestId, getHookId) if (setTestsById) { // use callback here to hand back @@ -420,7 +441,7 @@ const normalizeAll = (suite, initialTests = {}, setTestsById, setTests, onRunnab return normalizedSuite } -const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTestId) => { +const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTestId, getHookId) => { const normalizeRunnable = (runnable) => { let i @@ -446,6 +467,9 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTes _.extend(runnable, i) } + // merge all hooks into single array + runnable.hooks = condenseHooks(runnable, getHookId) + // reduce this runnable down to its props // and collections return wrapAll(runnable) @@ -466,7 +490,7 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTes _.each({ tests: runnable.tests, suites: runnable.suites }, (_runnables, type) => { if (runnable[type]) { return normalizedRunnable[type] = _.map(_runnables, (runnable) => { - return normalize(runnable, tests, initialTests, onRunnable, onLogsById, getTestId) + return normalize(runnable, tests, initialTests, onRunnable, onLogsById, getTestId, getHookId) }) } }) @@ -480,7 +504,7 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTes if (suite._onlyTests.length) { suite.tests = suite._onlyTests normalizedSuite.tests = _.map(suite._onlyTests, (test) => { - const normalizedTest = normalizeRunnable(test, initialTests, onRunnable, onLogsById, getTestId) + const normalizedTest = normalizeRunnable(test, initialTests, onRunnable, onLogsById, getTestId, getHookId) push(normalizedTest) @@ -493,7 +517,7 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTes suite.tests = [] normalizedSuite.tests = [] _.each(suite._onlySuites, (onlySuite) => { - const normalizedOnlySuite = normalizeRunnable(onlySuite, initialTests, onRunnable, onLogsById, getTestId) + const normalizedOnlySuite = normalizeRunnable(onlySuite, initialTests, onRunnable, onLogsById, getTestId, getHookId) if (hasOnly(onlySuite)) { return filterOnly(normalizedOnlySuite, onlySuite) @@ -501,12 +525,12 @@ const normalize = (runnable, tests, initialTests, onRunnable, onLogsById, getTes }) suite.suites = _.filter(suite.suites, (childSuite) => { - const normalizedChildSuite = normalizeRunnable(childSuite, initialTests, onRunnable, onLogsById, getTestId) + const normalizedChildSuite = normalizeRunnable(childSuite, initialTests, onRunnable, onLogsById, getTestId, getHookId) return (suite._onlySuites.indexOf(childSuite) !== -1) || filterOnly(normalizedChildSuite, childSuite) }) - normalizedSuite.suites = _.map(suite.suites, (childSuite) => normalize(childSuite, tests, initialTests, onRunnable, onLogsById, getTestId)) + normalizedSuite.suites = _.map(suite.suites, (childSuite) => normalize(childSuite, tests, initialTests, onRunnable, onLogsById, getTestId, getHookId)) } return suite.tests.length || suite.suites.length @@ -585,14 +609,6 @@ const _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, se }) _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/after all's. // if there is a nested suite with a before, then // currentTest will refer to the previous test run @@ -903,6 +919,7 @@ const create = (specWindow, mocha, Cypress, cy) => { onRunnable, onLogsById, getTestId, + getHookId, ) }, @@ -968,6 +985,9 @@ const create = (specWindow, mocha, Cypress, cy) => { // if this isnt a hook, then the name is 'test' const hookName = runnable.type === 'hook' ? getHookName(runnable) : 'test' + // set hook id to hook id or test id + const hookId = runnable.type === 'hook' ? runnable.hookId : runnable.id + // 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 @@ -1061,7 +1081,7 @@ const create = (specWindow, mocha, Cypress, cy) => { // 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, hookId) // TODO: handle promise timeouts here! // whenever any runnable is about to run diff --git a/packages/driver/src/cypress/stack_utils.js b/packages/driver/src/cypress/stack_utils.js index 59b8a07a48dd..ccb7daac37b4 100644 --- a/packages/driver/src/cypress/stack_utils.js +++ b/packages/driver/src/cypress/stack_utils.js @@ -241,6 +241,14 @@ const getSourceDetailsForLine = (projectRoot, line) => { } } +const getSourceDetailsForFirstLine = (stack, projectRoot) => { + const line = getStackLines(stack)[0] + + if (!line) return + + return getSourceDetailsForLine(projectRoot, line) +} + const reconstructStack = (parsedStack) => { return _.map(parsedStack, (parsedLine) => { if (parsedLine.message != null) { @@ -330,6 +338,7 @@ module.exports = { getCodeFrame, getSourceStack, getStackLines, + getSourceDetailsForFirstLine, hasCrossFrameStacks, normalizedStack, normalizedUserInvocationStack, diff --git a/packages/reporter/cypress/fixtures/runnables.json b/packages/reporter/cypress/fixtures/runnables.json index 29f022d400d5..7c8073254097 100644 --- a/packages/reporter/cypress/fixtures/runnables.json +++ b/packages/reporter/cypress/fixtures/runnables.json @@ -2,14 +2,14 @@ "id": "r1", "title": "", "root": true, - "tests": [ - - ], + "hooks": [], + "tests": [], "suites": [ { "id": "r2", "title": "suite 1", "root": false, + "hooks": [], "tests": [ { "id": "r3", @@ -27,6 +27,7 @@ "id": "r5", "title": "nested suite 1", "root": false, + "hooks": [], "tests": [ { "id": "r6", @@ -39,7 +40,7 @@ "state": "active", "commands": [ { - "hookName": "test", + "hookId": "r7", "id": "c1", "instrument": "command", "message": "http://localhost:3000", diff --git a/packages/reporter/cypress/fixtures/runnables_aliases.json b/packages/reporter/cypress/fixtures/runnables_aliases.json index 6626864d36a9..380ad4001e9e 100644 --- a/packages/reporter/cypress/fixtures/runnables_aliases.json +++ b/packages/reporter/cypress/fixtures/runnables_aliases.json @@ -2,6 +2,7 @@ "id": "r1", "title": "", "root": true, + "hooks": [], "suites": [], "tests": [ { diff --git a/packages/reporter/cypress/fixtures/runnables_error.json b/packages/reporter/cypress/fixtures/runnables_error.json index 0f79cc2d6baf..21ff466dc324 100644 --- a/packages/reporter/cypress/fixtures/runnables_error.json +++ b/packages/reporter/cypress/fixtures/runnables_error.json @@ -3,11 +3,13 @@ "title": "", "root": true, "tests": [], + "hooks": [], "suites": [ { "id": "r2", "title": "suite 1", "root": false, + "hooks": [], "tests": [ { "id": "r3", @@ -20,7 +22,7 @@ }, "commands": [ { - "hookName": "test", + "hookId": "r3", "id": "c1", "instrument": "command", "message": "http://localhost:3000", diff --git a/packages/reporter/cypress/fixtures/runnables_hooks.json b/packages/reporter/cypress/fixtures/runnables_hooks.json new file mode 100644 index 000000000000..e20dba70b855 --- /dev/null +++ b/packages/reporter/cypress/fixtures/runnables_hooks.json @@ -0,0 +1,199 @@ +{ + "id": "r1", + "title": "", + "root": true, + "hooks": [ + { + "hookId": "h1", + "hookName": "before each", + "invocationDetails": { + "absoluteFile": "/absolute/path/to/foo_spec.js", + "column": 4, + "line": 10, + "originalFile": "path/to/foo_spec.js", + "relativeFile": "path/to/foo_spec.js" + } + } + ], + "tests": [], + "suites": [ + { + "id": "r2", + "title": "suite 1", + "root": false, + "hooks": [ + { + "hookId": "h2", + "hookName": "after each" + } + ], + "tests": [ + { + "id": "r3", + "title": "test 1", + "state": "passed", + "commands": [ + { + "hookId": "h1", + "id": "c1", + "instrument": "command", + "message": "http://localhost:3000", + "name": "visit", + "state": "passed", + "testId": "r3", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "h1", + "id": "c2", + "instrument": "command", + "message": ".wrapper", + "name": "get", + "state": "passed", + "testId": "r3", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "r3", + "id": "c3", + "instrument": "command", + "message": ".body", + "name": "get", + "state": "passed", + "testId": "r3", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "h2", + "id": "c4", + "instrument": "command", + "message": ".cleanup", + "name": "get", + "state": "passed", + "testId": "r3", + "timeout": 4000, + "type": "parent" + } + ], + "invocationDetails": { + "absoluteFile": "/absolute/path/to/foo_spec.js", + "column": 8, + "line": 34, + "originalFile": "path/to/foo_spec.js", + "relativeFile": "path/to/foo_spec.js" + } + } + ], + "suites": [ + { + "id": "r4", + "title": "nested suite 1", + "root": false, + "hooks": [ + { + "hookId": "h3", + "hookName": "before all" + }, + { + "hookId": "h4", + "hookName": "before all" + }, + { + "hookId": "h5", + "hookName": "before each" + } + ], + "tests": [ + { + "id": "r5", + "title": "test 2", + "state": "passed", + "commands": [ + { + "hookId": "h3", + "id": "c5", + "instrument": "command", + "message": "before1", + "name": "log", + "state": "passed", + "testId": "r5", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "h4", + "id": "c5", + "instrument": "command", + "message": "before2", + "name": "log", + "state": "passed", + "testId": "r5", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "h1", + "id": "c5", + "instrument": "command", + "message": "http://localhost:3000", + "name": "visit", + "state": "passed", + "testId": "r5", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "h1", + "id": "c6", + "instrument": "command", + "message": ".wrapper", + "name": "get", + "state": "passed", + "testId": "r5", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "h5", + "id": "c7", + "instrument": "command", + "message": ".header", + "name": "get", + "state": "passed", + "testId": "r5", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "r5", + "id": "c8", + "instrument": "command", + "message": ".body", + "name": "get", + "state": "passed", + "testId": "r5", + "timeout": 4000, + "type": "parent" + }, + { + "hookId": "h2", + "id": "c9", + "instrument": "command", + "message": ".cleanup", + "name": "get", + "state": "passed", + "testId": "r5", + "timeout": 4000, + "type": "parent" + } + ] + } + ] + } + ] + } + ] +} diff --git a/packages/reporter/cypress/integration/aliases_spec.js b/packages/reporter/cypress/integration/aliases_spec.js index 01b2eef7b7ac..852860b23275 100644 --- a/packages/reporter/cypress/integration/aliases_spec.js +++ b/packages/reporter/cypress/integration/aliases_spec.js @@ -4,7 +4,7 @@ const { _ } = Cypress const addLog = function (runner, log) { const defaultLog = { event: false, - hookName: 'test', + hookId: 'r3', id: _.uniqueId('l'), instrument: 'command', renderProps: {}, diff --git a/packages/reporter/cypress/integration/hooks_spec.ts b/packages/reporter/cypress/integration/hooks_spec.ts new file mode 100644 index 000000000000..ce3d783a7a18 --- /dev/null +++ b/packages/reporter/cypress/integration/hooks_spec.ts @@ -0,0 +1,113 @@ +import { EventEmitter } from 'events' +import { itHandlesFileOpening } from '../support/utils' + +describe('hooks', function () { + beforeEach(function () { + cy.fixture('runnables_hooks').as('runnables') + + this.runner = new EventEmitter() + + cy.visit('/dist').then((win) => { + win.render({ + runner: this.runner, + spec: { + name: 'foo.js', + relative: 'relative/path/to/foo.js', + absolute: '/absolute/path/to/foo.js', + }, + }) + }) + + cy.get('.reporter').then(() => { + this.runner.emit('runnables:ready', this.runnables) + + this.runner.emit('reporter:start', {}) + }) + }) + + describe('group hooks', function () { + beforeEach(function () { + cy.contains('test 1').click() + }) + + it('assigns commands to the correct hook', function () { + cy.contains('before each').closest('.collapsible').find('.command').should('have.length', 2) + cy.contains('before each').closest('.collapsible').should('contain', 'http://localhost:3000') + cy.contains('before each').closest('.collapsible').should('contain', '.wrapper') + + cy.contains('test body').closest('.collapsible').find('.command').should('have.length', 1) + cy.contains('test body').closest('.collapsible').should('contain', '.body') + + cy.contains('after each').closest('.collapsible').find('.command').should('have.length', 1) + cy.contains('after each').closest('.collapsible').should('contain', '.cleanup') + }) + + it('displays hooks in the correct order', function () { + const hooks = ['before each', 'test body', 'after each'] + + cy.get('.hooks-container').find('span.hook-name').each(function (name, i) { + cy.wrap(name).should('contain', hooks[i]) + }) + }) + }) + + describe('split hooks', function () { + beforeEach(function () { + cy.contains('test 2').click() + }) + + it('splits different hooks with the same name', function () { + cy.contains('before all (1)').closest('.collapsible').find('.command').should('have.length', 1) + cy.contains('before all (1)').closest('.collapsible').should('contain', 'before1') + + cy.contains('before all (2)').closest('.collapsible').find('.command').should('have.length', 1) + cy.contains('before all (2)').closest('.collapsible').should('contain', 'before2') + + cy.contains('before each (1)').closest('.collapsible').find('.command').should('have.length', 2) + cy.contains('before each (1)').closest('.collapsible').should('contain', 'http://localhost:3000') + cy.contains('before each (1)').closest('.collapsible').should('contain', '.wrapper') + + cy.contains('before each (2)').closest('.collapsible').find('.command').should('have.length', 1) + cy.contains('before each (2)').closest('.collapsible').should('contain', '.header') + }) + + it('does not display hook number when only one', function () { + cy.get('.hooks-container').should('contain', 'after each') + cy.get('.hooks-container').should('not.contain', 'after each (1)') + }) + }) + + describe('open hooks in IDE', function () { + beforeEach(function () { + cy.contains('test 1').click() + }) + + it('does not display button without hover', function () { + cy.contains('Open in IDE').should('not.be.visible') + }) + + it('creates button when hook has invocation details', function () { + cy.contains('before each').closest('.hook-header').should('contain', 'Open in IDE') + }) + + it('creates button when test has invocation details', function () { + cy.contains('test body').closest('.hook-header').should('contain', 'Open in IDE') + }) + + it('does not create button when hook does not have invocation details', function () { + cy.contains('after each').closest('.hook-header').should('not.contain', 'Open in IDE') + }) + + describe('handles file opening', function () { + beforeEach(function () { + cy.get('.hook-open-in-ide').first().invoke('show') + }) + + itHandlesFileOpening('.hook-open-in-ide', { + file: '/absolute/path/to/foo_spec.js', + column: 4, + line: 10, + }) + }) + }) +}) diff --git a/packages/reporter/cypress/integration/test_errors_spec.js b/packages/reporter/cypress/integration/test_errors_spec.js index be5649f583c3..b927fb36bad5 100644 --- a/packages/reporter/cypress/integration/test_errors_spec.js +++ b/packages/reporter/cypress/integration/test_errors_spec.js @@ -239,7 +239,14 @@ describe('test errors', function () { cy.get('.command-wrapper').should('be.visible') }) - itHandlesFileOpening('.runnable-err-stack-trace', { + it('displays tooltip on hover', () => { + cy.contains('View stack trace').click() + + cy.get('.runnable-err-stack-trace a').first().trigger('mouseover') + cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE') + }) + + itHandlesFileOpening('.runnable-err-stack-trace a', { file: '/me/dev/my/app.js', line: 2, column: 7, @@ -326,7 +333,12 @@ describe('test errors', function () { .should('have.class', 'language-text') }) - itHandlesFileOpening('.test-err-code-frame', { + it('displays tooltip on hover', () => { + cy.get('.test-err-code-frame a').first().trigger('mouseover') + cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE') + }) + + itHandlesFileOpening('.test-err-code-frame a', { file: '/me/dev/my/app.js', line: 2, column: 7, diff --git a/packages/reporter/cypress/integration/tests_spec.ts b/packages/reporter/cypress/integration/tests_spec.ts index c7d0cd48d747..48880d9863e5 100644 --- a/packages/reporter/cypress/integration/tests_spec.ts +++ b/packages/reporter/cypress/integration/tests_spec.ts @@ -118,7 +118,12 @@ describe('controls', function () { cy.get('.runnable-header').find('a').should('have.text', 'relative/path/to/foo.js') }) - itHandlesFileOpening('.runnable-header', { + it('displays tooltip on hover', () => { + cy.get('.runnable-header a').first().trigger('mouseover') + cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE') + }) + + itHandlesFileOpening('.runnable-header a', { file: '/absolute/path/to/foo.js', line: 0, column: 0, diff --git a/packages/reporter/cypress/support/utils.js b/packages/reporter/cypress/support/utils.js index 20cf5f9dda8a..8ff69dbe27a9 100644 --- a/packages/reporter/cypress/support/utils.js +++ b/packages/reporter/cypress/support/utils.js @@ -1,19 +1,10 @@ const _ = Cypress._ -export const itHandlesFileOpening = (containerSelector, file, stackTrace = false) => { +export const itHandlesFileOpening = (selector, file, stackTrace = false) => { beforeEach(function () { cy.stub(this.runner, 'emit').callThrough() }) - it('displays tooltip on hover', () => { - if (stackTrace) { - cy.contains('View stack trace').click() - } - - cy.get(`${containerSelector} a`).first().trigger('mouseover') - cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE') - }) - describe('when user has already set opener and opens file', function () { beforeEach(function () { this.editor = {} @@ -28,7 +19,7 @@ export const itHandlesFileOpening = (containerSelector, file, stackTrace = false }) it('opens in preferred opener', function () { - cy.get(`${containerSelector} a`).first().click().then(() => { + cy.get(selector).first().click().then(() => { expect(this.runner.emit).to.be.calledWith('open:file', { where: this.editor, ...file, @@ -56,7 +47,7 @@ export const itHandlesFileOpening = (containerSelector, file, stackTrace = false cy.contains('View stack trace').click() } - cy.get(`${containerSelector} a`).first().click() + cy.get(selector).first().click() }) it('opens modal with available editors', function () { diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index 20ff575ccb03..bab7cca686ff 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -20,7 +20,7 @@ export interface CommandProps extends InstrumentProps { timeout: number visible?: boolean wallClockStartedAt: string - hookName: string + hookId: string } export default class Command extends Instrument { @@ -35,6 +35,7 @@ export default class Command extends Instrument { @observable wallClockStartedAt: string @observable duplicates: Array = [] @observable isDuplicate = false + @observable hookId: string private _prevState: string | null | undefined = null private _pendingTimeout?: TimeoutID = undefined @@ -63,6 +64,7 @@ export default class Command extends Instrument { this.timeout = props.timeout this.visible = props.visible this.wallClockStartedAt = props.wallClockStartedAt + this.hookId = props.hookId this._checkLongRunning() } diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index cd89088e04c7..032457d68618 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -16,35 +16,54 @@ margin-right: 3px; } - .hook-name > .collapsible-header { - border-bottom: 1px solid transparent; - text-transform: uppercase; - color: #959595; - display: inline-block; - font-size: 11px; - cursor: pointer; - - &:focus { - outline: 1px dotted #6c6c6c; - } + .hook-header { + display: flex; + width: 100%; &:hover { - border-bottom: 1px dotted #959595; - color: #333; - } + .collapsible-header { + color: #6a6b6c; + } - &:hover:focus { - border-bottom: 1px dotted #959595; - color: #333; + .hook-open-in-ide { + display: block; + } } - > .collapsible-header-inner:focus { - outline: 0; + .collapsible-header { + text-transform: uppercase; + color: #959595; + display: inline-block; + flex-grow: 1; + font-size: 11px; + cursor: pointer; + padding: 4px 0; + + &:focus { + outline: 1px dotted #6c6c6c; + } + + > .collapsible-header-inner:focus { + outline: 0; + } + + .hook-failed-message { + color: #E94F5F; + display: none; + } } - .hook-failed-message { - color: #E94F5F; + .hook-open-in-ide { + align-items: center; + color: #6a6b6c; display: none; + padding: 4px 10px; + + &:hover, &:focus { + background-color: rgba(186, 186, 186, 0.2); + outline: none; + text-decoration: none; + } } } diff --git a/packages/reporter/src/errors/error-code-frame.tsx b/packages/reporter/src/errors/error-code-frame.tsx index c1092f130a4b..1bcbf1c69dd8 100644 --- a/packages/reporter/src/errors/error-code-frame.tsx +++ b/packages/reporter/src/errors/error-code-frame.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react' import Prism from 'prismjs' import { CodeFrame } from './err-model' -import FileOpener from '../lib/file-opener' +import FileNameOpener from '../lib/file-name-opener' interface Props { codeFrame: CodeFrame @@ -24,7 +24,7 @@ class ErrorCodeFrame extends Component { return (
- +
           {frame}
         
diff --git a/packages/reporter/src/errors/error-stack.tsx b/packages/reporter/src/errors/error-stack.tsx index f51b2110cca9..579b27ea6117 100644 --- a/packages/reporter/src/errors/error-stack.tsx +++ b/packages/reporter/src/errors/error-stack.tsx @@ -2,7 +2,7 @@ import _ from 'lodash' import { observer } from 'mobx-react' import React, { ReactElement } from 'react' -import FileOpener from '../lib/file-opener' +import FileNameOpener from '../lib/file-name-opener' import Err from './err-model' const cypressLineRegex = /(cypress:\/\/|cypress_runner\.js)/ @@ -62,7 +62,7 @@ const ErrorStack = observer(({ err }: Props) => { } const link = ( - + ) return makeLine(key, [whitespace, `at ${fn} (`, link, ')']) diff --git a/packages/reporter/src/hooks/hook-model.spec.ts b/packages/reporter/src/hooks/hook-model.spec.ts index 21f33f42968e..06a459a80bab 100644 --- a/packages/reporter/src/hooks/hook-model.spec.ts +++ b/packages/reporter/src/hooks/hook-model.spec.ts @@ -9,13 +9,10 @@ describe('Hook model', () => { let hook: Hook beforeEach(() => { - hook = new Hook({ name: 'before' }) - }) - - it('gives hooks unique ids', () => { - const anotherHook = new Hook({ name: 'test' }) - - expect(hook.id).not.to.equal(anotherHook.id) + hook = new Hook({ + hookId: 'h1', + hookName: 'before each', + }) }) context('#addCommand', () => { @@ -151,7 +148,7 @@ describe('Hook model', () => { return hook.addCommand(command as CommandModel) } - it('returns duplicates marked with hasDuplicates and those that appear mulitple times in the commands array', () => { + it('returns duplicates marked with hasDuplicates and those that appear multiple times in the commands array', () => { addCommand('foo') addCommand('bar') addCommand('foo') diff --git a/packages/reporter/src/hooks/hook-model.ts b/packages/reporter/src/hooks/hook-model.ts index ee454dd3e0d2..e9bae5b296b4 100644 --- a/packages/reporter/src/hooks/hook-model.ts +++ b/packages/reporter/src/hooks/hook-model.ts @@ -1,22 +1,36 @@ import _ from 'lodash' import { observable, computed } from 'mobx' +import { FileDetails } from '@packages/ui-components' + import { Alias } from '../instruments/instrument-model' import Err from '../errors/err-model' import CommandModel from '../commands/command-model' -export default class Hook { - @observable id: string - @observable name: string +export type HookName = 'before all' | 'before each' | 'after all' | 'after each' | 'test body' + +export interface HookProps { + hookId: string + hookName: HookName + invocationDetails?: FileDetails +} + +export default class Hook implements HookProps { + @observable hookId: string + @observable hookName: HookName + @observable hookNumber?: number + @observable invocationDetails?: FileDetails + @observable invocationOrder?: number @observable commands: Array = [] @observable failed = false private _aliasesWithDuplicatesCache: Array | null = null private _currentNumber = 1 - constructor (props: { name: string }) { - this.id = _.uniqueId('h') - this.name = props.name + constructor (props: HookProps) { + this.hookId = props.hookId + this.hookName = props.hookName + this.invocationDetails = props.invocationDetails } @computed get aliasesWithDuplicates () { diff --git a/packages/reporter/src/hooks/hooks.spec.tsx b/packages/reporter/src/hooks/hooks.spec.tsx index 3144f4f6add5..9c89eb43bf9a 100644 --- a/packages/reporter/src/hooks/hooks.spec.tsx +++ b/packages/reporter/src/hooks/hooks.spec.tsx @@ -11,8 +11,8 @@ const commandModel = () => { const hookModel = (props?: Partial) => { return _.extend({ - id: _.uniqueId('h'), - name: 'before', + hookId: _.uniqueId('h'), + hookName: 'before each', failed: false, commands: [commandModel(), commandModel()], }, props) @@ -21,6 +21,13 @@ const hookModel = (props?: Partial) => { const model = (props?: Partial) => { return _.extend({ hooks: [hookModel(), hookModel(), hookModel()], + hookCount: { + 'before all': 0, + 'before each': 3, + 'after all': 0, + 'after each': 0, + 'test body': 0, + }, }, props) } @@ -31,34 +38,55 @@ describe('', () => { expect(component.find(Hook).length).to.equal(3) }) + it('renders a number when there are multiple hooks of the same name', () => { + const component = shallow() + + expect(component.find(Hook).first()).to.have.prop('showNumber', true) + }) + + it('does not render a number when there are not multiple hooks of the same name', () => { + const component = shallow() + + expect(component.find(Hook).first()).to.have.prop('showNumber', false) + }) + context('', () => { it('renders without hook-failed class when not failed', () => { - const component = shallow() + const component = shallow() expect(component).not.to.have.className('hook-failed') }) it('renders with hook-failed class when failed', () => { - const component = shallow() + const component = shallow() expect(component).to.have.className('hook-failed') }) it('renders Collapsible with hook header', () => { - const component = shallow() + const component = shallow() const header = shallow(component.find('Collapsible').prop('header')) expect(header.find('.hook-failed-message')).to.have.text('(failed)') }) it('renders Collapsible open', () => { - const component = shallow() + const component = shallow() expect(component.find('Collapsible').prop('isOpen')).to.be.true }) it('renders command for each in model', () => { - const component = shallow() + const component = shallow() expect(component.find('Command').length).to.equal(2) }) @@ -76,5 +104,11 @@ describe('', () => { expect(component.find('.hook-failed-message')).to.have.text('(failed)') }) + + it('renders the number', () => { + const component = shallow() + + expect(component.text()).to.contain('before (1)') + }) }) }) diff --git a/packages/reporter/src/hooks/hooks.tsx b/packages/reporter/src/hooks/hooks.tsx index 65a4422b3544..f543dace89d2 100644 --- a/packages/reporter/src/hooks/hooks.tsx +++ b/packages/reporter/src/hooks/hooks.tsx @@ -2,29 +2,45 @@ import cs from 'classnames' import _ from 'lodash' import { observer } from 'mobx-react' import React from 'react' +import { FileDetails } from '@packages/ui-components' + import Command from '../commands/command' import Collapsible from '../collapsible/collapsible' -import HookModel from './hook-model' +import HookModel, { HookName } from './hook-model' +import FileOpener from '../lib/file-opener' export interface HookHeaderProps { name: string + number?: number } -const HookHeader = ({ name }: HookHeaderProps) => ( - - {name === 'test' ? 'test body' : name} (failed) +const HookHeader = ({ name, number }: HookHeaderProps) => ( + + {name} {number && `(${number})`} (failed) ) +export interface HookOpenInIDEProps { + invocationDetails: FileDetails +} + +const HookOpenInIDE = ({ invocationDetails }: HookOpenInIDEProps) => ( + + Open in IDE + +) + export interface HookProps { model: HookModel + showNumber: boolean } -const Hook = observer(({ model }: HookProps) => ( +const Hook = observer(({ model, showNumber }: HookProps) => (
  • } - headerClass='hook-name' + header={} + headerClass='hook-header' + headerExtras={model.invocationDetails && } isOpen={true} >
      @@ -36,6 +52,7 @@ const Hook = observer(({ model }: HookProps) => ( export interface HooksModel { hooks: Array + hookCount: { [name in HookName]: number } } export interface HooksProps { @@ -44,7 +61,7 @@ export interface HooksProps { const Hooks = observer(({ model }: HooksProps) => (
        - {_.map(model.hooks, (hook) => )} + {_.map(model.hooks, (hook) => hook.commands.length ? 1} /> : undefined)}
      )) diff --git a/packages/reporter/src/lib/file-name-opener.tsx b/packages/reporter/src/lib/file-name-opener.tsx new file mode 100644 index 000000000000..a9d854560c4a --- /dev/null +++ b/packages/reporter/src/lib/file-name-opener.tsx @@ -0,0 +1,28 @@ +import { observer } from 'mobx-react' +import React from 'react' +// @ts-ignore +import Tooltip from '@cypress/react-tooltip' +import { FileDetails } from '@packages/ui-components' + +import FileOpener from './file-opener' + +interface Props { + fileDetails: FileDetails, + className?: string +} + +const FileNameOpener = observer((props: Props) => { + const { originalFile, line, column } = props.fileDetails + + return ( + + + + {originalFile}{!!line && `:${line}`}{!!column && `:${column}`} + + + + ) +}) + +export default FileNameOpener diff --git a/packages/reporter/src/lib/file-opener.tsx b/packages/reporter/src/lib/file-opener.tsx index 1c77acca93ba..7ab34d29f320 100644 --- a/packages/reporter/src/lib/file-opener.tsx +++ b/packages/reporter/src/lib/file-opener.tsx @@ -1,14 +1,12 @@ import { observer } from 'mobx-react' -import React from 'react' -// @ts-ignore -import Tooltip from '@cypress/react-tooltip' +import React, { ReactNode } from 'react' +import { GetUserEditorResult, Editor, FileDetails, FileOpener as Opener } from '@packages/ui-components' import events from './events' -import { GetUserEditorResult, Editor, FileDetails, FileOpener as Opener } from '@packages/ui-components' - interface Props { - fileDetails: FileDetails, + fileDetails: FileDetails + children: ReactNode className?: string } @@ -29,23 +27,16 @@ const setUserEditor = (editor: Editor) => { events.emit('set:user:editor', editor) } -const FileOpener = observer((props: Props) => { - const { originalFile, line, column } = props.fileDetails - - return ( - - - - {originalFile}{!!line && `:${line}`}{!!column && `:${column}`} - - - - ) -}) +const FileOpener = observer(({ fileDetails, children, className }: Props) => ( + + {children} + +)) export default FileOpener diff --git a/packages/reporter/src/runnables/runnable-header.tsx b/packages/reporter/src/runnables/runnable-header.tsx index cecda34d5dbb..2f57fd2e5708 100644 --- a/packages/reporter/src/runnables/runnable-header.tsx +++ b/packages/reporter/src/runnables/runnable-header.tsx @@ -1,6 +1,6 @@ import React, { Component, ReactElement } from 'react' -import FileOpener from '../lib/file-opener' +import FileNameOpener from '../lib/file-name-opener' const renderRunnableHeader = (children:ReactElement) =>
      {children}
      @@ -28,7 +28,7 @@ class RunnableHeader extends Component { } return renderRunnableHeader( - , + , ) } } diff --git a/packages/reporter/src/runnables/runnable-model.ts b/packages/reporter/src/runnables/runnable-model.ts index 39189e519bdc..91c511632b38 100644 --- a/packages/reporter/src/runnables/runnable-model.ts +++ b/packages/reporter/src/runnables/runnable-model.ts @@ -1,8 +1,10 @@ import { observable } from 'mobx' +import { HookProps } from '../hooks/hook-model' export interface RunnableProps { id: number title?: string + hooks: Array } export default class Runnable { @@ -10,10 +12,12 @@ export default class Runnable { @observable shouldRender: boolean = false @observable title?: string @observable level: number + @observable hooks: Array = [] constructor (props: RunnableProps, level: number) { this.id = props.id this.title = props.title this.level = level + this.hooks = props.hooks } } diff --git a/packages/reporter/src/runnables/runnables-store.spec.ts b/packages/reporter/src/runnables/runnables-store.spec.ts index 074c87e481f5..1ba679979ff3 100644 --- a/packages/reporter/src/runnables/runnables-store.spec.ts +++ b/packages/reporter/src/runnables/runnables-store.spec.ts @@ -9,6 +9,7 @@ import TestModel, { TestProps } from '../test/test-model' import { AgentProps } from '../agents/agent-model' import { CommandProps } from '../commands/command-model' import { RouteProps } from '../routes/route-model' +import { HookProps } from '../hooks/hook-model' const appStateStub = () => { return { @@ -29,17 +30,20 @@ const scrollerStub = () => { } as ScrollerStub } +const createHook = (hookId: string) => { + return { hookId, hookName: 'before each' } as HookProps +} const createTest = (id: number) => { - return { id, title: `test ${id}` } as TestProps + return { id, title: `test ${id}`, hooks: [], state: 'processing' } as TestProps } const createSuite = (id: number, tests: Array, suites: Array) => { - return { id, title: `suite ${id}`, tests, suites } as SuiteProps + return { id, title: `suite ${id}`, tests, suites, hooks: [] } as SuiteProps } const createAgent = (id: number, testId: number) => { return { id, testId, instrument: 'agent' } as AgentProps } -const createCommand = (id: number, testId: number) => { - return { id, testId, instrument: 'command' } as CommandProps +const createCommand = (id: number, testId: number, hookId?: string) => { + return { id, testId, instrument: 'command', hookId } as CommandProps } const createRoute = (id: number, testId: number) => { return { id, testId, instrument: 'route' } as RouteProps @@ -95,8 +99,9 @@ describe('runnables store', () => { const rootRunnable = createRootRunnable() rootRunnable.tests![0].agents = [createAgent(1, 1), createAgent(2, 1), createAgent(3, 1)] - rootRunnable.tests![0].commands = [createCommand(1, 1)] + rootRunnable.tests![0].commands = [createCommand(1, 1, 'h1')] rootRunnable.tests![0].routes = [createRoute(1, 1), createRoute(2, 1)] + rootRunnable.tests![0].hooks = [createHook('h1')] instance.setRunnables(rootRunnable) expect((instance.runnables[0] as TestModel).agents.length).to.equal(3) expect((instance.runnables[0] as TestModel).commands.length).to.equal(1) @@ -111,6 +116,19 @@ describe('runnables store', () => { expect(((instance.runnables[1] as SuiteModel).children[2] as SuiteModel).children[0].level).to.equal(2) }) + it('merges down hooks', () => { + const rootRunnable = createRootRunnable() + + rootRunnable.suites![0].hooks = [createHook('h1'), createHook('h2')] + rootRunnable.suites![0].suites[0].hooks = [createHook('h3')] + rootRunnable.suites![0].suites[0].tests[0].hooks = [createHook('h4')] + instance.setRunnables(rootRunnable) + expect(instance.runnables[0].hooks.length).to.equal(1) + expect(instance.runnables[1].hooks.length).to.equal(2) + expect((instance.runnables[1] as SuiteModel).children[2].hooks.length).to.equal(3) + expect(((instance.runnables[1] as SuiteModel).children[2] as SuiteModel).children[0].hooks.length).to.equal(5) + }) + it('sets .isReady flag', () => { instance.setRunnables({}) expect(instance.isReady).to.be.true @@ -206,8 +224,12 @@ describe('runnables store', () => { context('#updateLog', () => { it('updates the log', () => { - instance.setRunnables({ tests: [createTest(1)] }) - instance.addLog(createCommand(1, 1)) + const test = createTest(1) + + test.hooks = [createHook('h1')] + + instance.setRunnables({ tests: [test] }) + instance.addLog(createCommand(1, 1, 'h1')) instance.updateLog({ id: 1, name: 'new name' } as LogProps) expect(instance.testById(1).commands[0].name).to.equal('new name') }) diff --git a/packages/reporter/src/runnables/runnables-store.ts b/packages/reporter/src/runnables/runnables-store.ts index 8b88e2f4eed8..72a03eb382ba 100644 --- a/packages/reporter/src/runnables/runnables-store.ts +++ b/packages/reporter/src/runnables/runnables-store.ts @@ -6,6 +6,7 @@ import AgentModel, { AgentProps } from '../agents/agent-model' import CommandModel, { CommandProps } from '../commands/command-model' import RouteModel, { RouteProps } from '../routes/route-model' import scroller, { Scroller } from '../lib/scroller' +import { HookProps } from '../hooks/hook-model' import SuiteModel, { SuiteProps } from './suite-model' import TestModel, { TestProps, UpdateTestCallback } from '../test/test-model' import RunnableModel from './runnable-model' @@ -31,6 +32,7 @@ export type RunnableArray = Array type Log = AgentModel | CommandModel | RouteModel export interface RootRunnable { + hooks?: Array tests?: Array suites?: Array } @@ -73,18 +75,20 @@ class RunnablesStore { } _createRunnableChildren (runnableProps: RootRunnable, level: number) { - return this._createRunnables('test', runnableProps.tests || [], level).concat( - this._createRunnables('suite', runnableProps.suites || [], level), + return this._createRunnables('test', runnableProps.tests || [], runnableProps.hooks || [], level).concat( + this._createRunnables('suite', runnableProps.suites || [], runnableProps.hooks || [], level), ) } - _createRunnables (type: RunnableType, runnables: Array>, level: number) { + _createRunnables (type: RunnableType, runnables: Array>, hooks: Array, level: number) { return _.map(runnables, (runnableProps) => { - return this._createRunnable(type, runnableProps, level) + return this._createRunnable(type, runnableProps, hooks, level) }) } - _createRunnable (type: RunnableType, props: TestOrSuite, level: number) { + _createRunnable (type: RunnableType, props: TestOrSuite, hooks: Array, level: number) { + props.hooks = _.unionBy(props.hooks, hooks, 'hookId') + return type === 'suite' ? this._createSuite(props as SuiteProps, level) : this._createTest(props as TestProps, level) } @@ -176,7 +180,7 @@ class RunnablesStore { this._logs[log.id] = command this._withTest(log.testId, (test) => { - return test.addCommand(command, (log as CommandProps).hookName) + return test.addCommand(command) }) break diff --git a/packages/reporter/src/runnables/suite-model.spec.ts b/packages/reporter/src/runnables/suite-model.spec.ts index 46a4b6b17b24..ec10badaee74 100644 --- a/packages/reporter/src/runnables/suite-model.spec.ts +++ b/packages/reporter/src/runnables/suite-model.spec.ts @@ -2,7 +2,7 @@ import Suite from './suite-model' import TestModel from '../test/test-model' const suiteWithChildren = (children: Array>) => { - const suite = new Suite({ id: 1, title: '' }, 0) + const suite = new Suite({ id: 1, title: '', hooks: [] }, 0) suite.children = children as Array diff --git a/packages/reporter/src/test/test-model.spec.ts b/packages/reporter/src/test/test-model.spec.ts index 89588b14a207..9c9fb00e6533 100644 --- a/packages/reporter/src/test/test-model.spec.ts +++ b/packages/reporter/src/test/test-model.spec.ts @@ -1,3 +1,4 @@ +import { HookProps } from '../hooks/hook-model' import Command, { CommandProps } from '../commands/command-model' import Agent from '../agents/agent-model' import Route from '../routes/route-model' @@ -5,23 +6,32 @@ import Err from '../errors/err-model' import TestModel, { TestProps } from './test-model' +const commandHook: (hookId: string) => Partial = (hookId: string) => { + return { + hookId, + isMatchingEvent: () => { + return false + }, + } +} + describe('Test model', () => { context('.state', () => { it('is the "state" when it exists', () => { - const test = new TestModel({ state: 'passed' } as TestProps, 0) + const test = new TestModel({ id: 1, state: 'passed' } as TestProps, 0) expect(test.state).to.equal('passed') }) it('is active when there is no state and isActive is true', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.isActive = true expect(test.state).to.equal('active') }) it('is processing when there is no state and isActive is falsey', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) expect(test.state).to.equal('processing') }) @@ -29,31 +39,31 @@ describe('Test model', () => { context('.isLongRunning', () => { it('start out not long running', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) expect(test.isLongRunning).to.be.false }) it('is not long running if active but without a long running command', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.start() expect(test.isLongRunning).to.be.false }) it('becomes long running if active and has a long running command', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1, hooks: [{ hookId: 'h1' } as HookProps] } as TestProps, 0) test.start() - test.addCommand({ isLongRunning: true } as Command, '') + test.addCommand({ isLongRunning: true, hookId: 'h1' } as Command) expect(test.isLongRunning).to.be.true }) it('becomes not long running if it becomes inactive', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1, hooks: [{ hookId: 'h1' } as HookProps] } as TestProps, 0) test.start() - test.addCommand({ isLongRunning: true } as Command, '') + test.addCommand({ isLongRunning: true, hookId: 'h1' } as Command) test.finish({}) expect(test.isLongRunning).to.be.false }) @@ -61,7 +71,7 @@ describe('Test model', () => { context('#addAgent', () => { it('adds the agent to the agents collection', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.addAgent({} as Agent) expect(test.agents.length).to.equal(1) @@ -70,7 +80,7 @@ describe('Test model', () => { context('#addRoute', () => { it('adds the route to the routes collection', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.addRoute({} as Route) expect(test.routes.length).to.equal(1) @@ -79,39 +89,87 @@ describe('Test model', () => { context('#addCommand', () => { it('adds the command to the commands collection', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1, hooks: [{ hookId: 'h1' } as HookProps] } as TestProps, 0) - test.addCommand({} as Command, '') + test.addCommand({ hookId: 'h1' } as Command) expect(test.commands.length).to.equal(1) }) - it('creates a hook and adds the command to it if it does not exist', () => { - const test = new TestModel({} as TestProps, 0) + it('adds the command to the correct hook', () => { + const test = new TestModel({ + id: 1, + hooks: [ + { hookId: 'h1' } as HookProps, + { hookId: 'h2' } as HookProps, + ], + } as TestProps, 0) - test.addCommand({} as Command, 'some hook') - expect(test.hooks.length).to.equal(1) + test.addCommand(commandHook('h1') as Command) expect(test.hooks[0].commands.length).to.equal(1) - }) - - it('adds the command to an existing hook if it already exists', () => { - const test = new TestModel({} as TestProps, 0) - const command: Partial = { isMatchingEvent: () => { - return false - } } + expect(test.hooks[1].commands.length).to.equal(0) + expect(test.hooks[2].commands.length).to.equal(0) - test.addCommand(command as Command, 'some hook') - - expect(test.hooks.length).to.equal(1) + test.addCommand(commandHook('1') as Command) + expect(test.hooks[0].commands.length).to.equal(1) + expect(test.hooks[1].commands.length).to.equal(1) + expect(test.hooks[2].commands.length).to.equal(0) + }) + + it('moves hooks into the correct order', () => { + const test = new TestModel({ + id: 1, + hooks: [ + { hookId: 'h1' } as HookProps, + { hookId: 'h2' } as HookProps, + ], + } as TestProps, 0) + + test.addCommand(commandHook('h2') as Command) + expect(test.hooks[0].hookId).to.equal('h2') + expect(test.hooks[0].invocationOrder).to.equal(0) expect(test.hooks[0].commands.length).to.equal(1) - test.addCommand({} as Command, 'some hook') - expect(test.hooks.length).to.equal(1) - expect(test.hooks[0].commands.length).to.equal(2) + + test.addCommand(commandHook('h1') as Command) + expect(test.hooks[1].hookId).to.equal('h1') + expect(test.hooks[1].invocationOrder).to.equal(1) + expect(test.hooks[1].commands.length).to.equal(1) + }) + + it('counts and assigns the number of each hook type', () => { + const test = new TestModel({ + id: 1, + hooks: [ + { hookId: 'h1', hookName: 'before each' } as HookProps, + { hookId: 'h2', hookName: 'after each' } as HookProps, + { hookId: 'h3', hookName: 'before each' } as HookProps, + ], + } as TestProps, 0) + + test.addCommand(commandHook('h1') as Command) + expect(test.hookCount['before each']).to.equal(1) + expect(test.hookCount['after each']).to.equal(0) + expect(test.hooks[0].hookNumber).to.equal(1) + + test.addCommand(commandHook('h1') as Command) + expect(test.hookCount['before each']).to.equal(1) + expect(test.hookCount['after each']).to.equal(0) + expect(test.hooks[0].hookNumber).to.equal(1) + + test.addCommand(commandHook('h3') as Command) + expect(test.hookCount['before each']).to.equal(2) + expect(test.hookCount['after each']).to.equal(0) + expect(test.hooks[1].hookNumber).to.equal(2) + + test.addCommand(commandHook('h2') as Command) + expect(test.hookCount['before each']).to.equal(2) + expect(test.hookCount['after each']).to.equal(1) + expect(test.hooks[2].hookNumber).to.equal(1) }) }) context('#start', () => { it('sets the test as active', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.start() expect(test.isActive).to.be.true @@ -120,61 +178,61 @@ describe('Test model', () => { context('#finish', () => { it('sets the test as inactive', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.finish({}) expect(test.isActive).to.be.false }) it('updates the state of the test', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.finish({ state: 'failed' }) expect(test.state).to.equal('failed') }) it('updates the test err', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) test.finish({ err: { name: 'SomeError' } as Err }) expect(test.err.name).to.equal('SomeError') }) it('sets the hook to failed if it exists', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1, hooks: [{ hookId: 'h1' } as HookProps] } as TestProps, 0) - test.addCommand({} as Command, 'some hook') - test.finish({ hookName: 'some hook' }) + test.addCommand({ hookId: 'h1' } as Command) + test.finish({ hookId: 'h1' }) expect(test.hooks[0].failed).to.be.true }) it('does not throw error if hook does not exist', () => { - const test = new TestModel({} as TestProps, 0) + const test = new TestModel({ id: 1 } as TestProps, 0) expect(() => { - test.finish({ hookName: 'some hook' }) + test.finish({ hookId: 'h1' }) }).not.to.throw() }) }) context('#commandMatchingErr', () => { it('returns last command matching the error', () => { - const test = new TestModel({ err: { message: 'SomeError' } as Err } as TestProps, 0) + const test = new TestModel({ id: 1, err: { message: 'SomeError' } as Err, hooks: [{ hookId: 'h1' } as HookProps] } as TestProps, 0) - test.addCommand(new Command({ err: { message: 'SomeError' } as Err } as CommandProps), 'some hook') - test.addCommand(new Command({ err: {} as Err } as CommandProps), 'some hook') - test.addCommand(new Command({ err: { message: 'SomeError' } as Err } as CommandProps), 'some hook') - test.addCommand(new Command({ err: {} as Err } as CommandProps), 'another hook') - test.addCommand(new Command({ name: 'The One', err: { message: 'SomeError' } as Err } as CommandProps), 'another hook') + test.addCommand(new Command({ err: { message: 'SomeError' } as Err, hookId: 'h1' } as CommandProps)) + test.addCommand(new Command({ err: {} as Err, hookId: 'h1' } as CommandProps)) + test.addCommand(new Command({ err: { message: 'SomeError' } as Err, hookId: 'h1' } as CommandProps)) + test.addCommand(new Command({ err: {} as Err, hookId: 'h1' } as CommandProps)) + test.addCommand(new Command({ name: 'The One', err: { message: 'SomeError' } as Err, hookId: 'h1' } as CommandProps)) expect(test.commandMatchingErr()!.name).to.equal('The One') }) it('returns undefined if there are no commands with errors', () => { - const test = new TestModel({ err: { message: 'SomeError' } as Err } as TestProps, 0) + const test = new TestModel({ id: 1, err: { message: 'SomeError' } as Err, hooks: [{ hookId: 'h1' } as HookProps] } as TestProps, 0) - test.addCommand(new Command({} as CommandProps), 'some hook') - test.addCommand(new Command({} as CommandProps), 'some hook') - test.addCommand(new Command({} as CommandProps), 'another hook') + test.addCommand(new Command({ hookId: 'h1' } as CommandProps)) + test.addCommand(new Command({ hookId: 'h1' } as CommandProps)) + test.addCommand(new Command({ hookId: 'h1' } as CommandProps)) expect(test.commandMatchingErr()).to.be.undefined }) }) diff --git a/packages/reporter/src/test/test-model.ts b/packages/reporter/src/test/test-model.ts index 6baf62b82bd9..1be6515bf78e 100644 --- a/packages/reporter/src/test/test-model.ts +++ b/packages/reporter/src/test/test-model.ts @@ -1,8 +1,9 @@ import _ from 'lodash' import { action, autorun, computed, observable, observe } from 'mobx' +import { FileDetails } from '@packages/ui-components' import Err from '../errors/err-model' -import Hook from '../hooks/hook-model' +import Hook, { HookName } from '../hooks/hook-model' import Runnable, { RunnableProps } from '../runnables/runnable-model' import Command, { CommandProps } from '../commands/command-model' import Agent, { AgentProps } from '../agents/agent-model' @@ -19,12 +20,13 @@ export interface TestProps extends RunnableProps { agents?: Array commands?: Array routes?: Array + invocationDetails?: FileDetails } export interface UpdatableTestProps { state?: TestProps['state'] err?: TestProps['err'] - hookName?: string + hookId?: string isOpen?: TestProps['isOpen'] } @@ -39,6 +41,15 @@ export default class Test extends Runnable { @observable isOpen = false @observable routes: Array = [] @observable _state?: TestState | null = null + @observable _invocationCount: number = 0 + @observable invocationDetails?: FileDetails + @observable hookCount: { [name in HookName]: number } = { + 'before all': 0, + 'before each': 0, + 'after all': 0, + 'after each': 0, + 'test body': 0, + } type = 'test' callbackAfterUpdate: (() => void) | null = null @@ -49,6 +60,15 @@ export default class Test extends Runnable { this._state = props.state this.err.update(props.err) + this.invocationDetails = props.invocationDetails + + this.hooks = _.map(props.hooks, (hook) => new Hook(hook)) + this.hooks.push(new Hook({ + hookId: this.id.toString(), + hookName: 'test body', + invocationDetails: this.invocationDetails, + })) + autorun(() => { // if at any point, a command goes long running, set isLongRunning // to true until the test becomes inactive @@ -82,18 +102,36 @@ export default class Test extends Runnable { this.routes.push(route) } - addCommand (command: Command, hookName: string) { - const hook = this._findOrCreateHook(hookName) - + addCommand (command: Command) { this.commands.push(command) + + const hookIndex = _.findIndex(this.hooks, { hookId: command.hookId }) + + const hook = this.hooks[hookIndex] + hook.addCommand(command) + + // make sure that hooks are in order of invocation + if (hook.invocationOrder === undefined) { + hook.invocationOrder = this._invocationCount++ + + if (hook.invocationOrder !== hookIndex) { + this.hooks[hookIndex] = this.hooks[hook.invocationOrder] + this.hooks[hook.invocationOrder] = hook + } + } + + // assign number if non existent + if (hook.hookNumber === undefined) { + hook.hookNumber = ++this.hookCount[hook.hookName] + } } start () { this.isActive = true } - update ({ state, err, hookName, isOpen }: UpdatableTestProps, cb?: UpdateTestCallback) { + update ({ state, err, hookId, isOpen }: UpdatableTestProps, cb?: UpdateTestCallback) { let hadChanges = false const disposer = observe(this, (change) => { @@ -118,8 +156,8 @@ export default class Test extends Runnable { this.isOpen = isOpen } - if (hookName) { - const hook = _.find(this.hooks, { name: hookName }) + if (hookId) { + const hook = _.find(this.hooks, { hookId }) if (hook) { hook.failed = true @@ -154,16 +192,4 @@ export default class Test extends Runnable { .compact() .last() } - - _findOrCreateHook (name: string) { - 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/test/test.spec.tsx b/packages/reporter/src/test/test.spec.tsx index c01160ebaa4d..0dca44f42814 100644 --- a/packages/reporter/src/test/test.spec.tsx +++ b/packages/reporter/src/test/test.spec.tsx @@ -21,6 +21,7 @@ const model = (props?: Partial) => { return _.extend({ agents: [], commands: [], + hooks: [], err: {}, id: 't1', isActive: true, @@ -112,13 +113,13 @@ describe('', () => { }) it('renders if there are commands', () => { - const component = mount() + const component = shallow() expect(component.find(Hooks)).to.exist }) it('renders is no commands', () => { - const component = mount() + const component = shallow() expect(component.find(NoCommands)).to.exist }) diff --git a/packages/runner/__snapshots__/runner.mochaEvents.spec.js b/packages/runner/__snapshots__/runner.mochaEvents.spec.js index e57eb46540f5..97daa2d5fa39 100644 --- a/packages/runner/__snapshots__/runner.mochaEvents.spec.js +++ b/packages/runner/__snapshots__/runner.mochaEvents.spec.js @@ -38,7 +38,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "hookId": "h1", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -55,7 +56,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "type": "hook", "duration": "match.number", "file": null, - "originalTitle": "\"before all\" hook" + "originalTitle": "\"before all\" hook", + "invocationDetails": "{Object 8}" }, { "message": "[error message]", @@ -104,7 +106,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -166,7 +169,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "title": "test 1", "body": "[body]", "type": "test", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -179,7 +183,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "hookId": "h1", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -196,7 +201,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "type": "hook", "duration": "match.number", "file": null, - "originalTitle": "\"before each\" hook" + "originalTitle": "\"before each\" hook", + "invocationDetails": "{Object 8}" }, { "message": "[error message]", @@ -245,7 +251,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -307,7 +314,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "title": "test 1", "body": "[body]", "type": "test", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -336,7 +344,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -365,7 +374,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -378,7 +388,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "hookId": "h1", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -395,7 +406,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "type": "hook", "duration": "match.number", "file": null, - "originalTitle": "\"after each\" hook" + "originalTitle": "\"after each\" hook", + "invocationDetails": "{Object 8}" }, { "message": "[error message]", @@ -448,7 +460,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -510,7 +523,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "title": "test 1", "body": "[body]", "type": "test", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -532,7 +546,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "afterFnDuration": "match.number" } }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -554,7 +569,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "afterFnDuration": "match.number" } }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -577,7 +593,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "afterFnDuration": "match.number" } }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -589,7 +606,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "title": "test 2", "body": "[body]", "type": "test", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -618,7 +636,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -647,7 +666,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -660,7 +680,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "hookId": "h1", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -677,7 +698,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i "type": "hook", "duration": "match.number", "file": null, - "originalTitle": "\"after all\" hook" + "originalTitle": "\"after all\" hook", + "invocationDetails": "{Object 8}" }, { "message": "[error message]", @@ -730,7 +752,8 @@ exports['src/cypress/runner tests finish with correct state hook failures fail i } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -793,7 +816,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "hookId": "h1", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -807,7 +831,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -841,20 +866,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -867,7 +893,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "hookId": "h2", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -881,7 +908,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -918,20 +946,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" }, { "message": "[error message]", @@ -977,20 +1006,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1000,10 +1030,11 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "id": "r5", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1013,11 +1044,12 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "id": "r5", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1027,10 +1059,11 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "id": "r5", "title": "\"after all\" hook", "hookName": "after all", - "hookId": "h4", + "hookId": "h3", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1040,11 +1073,12 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with "id": "r5", "title": "\"after all\" hook", "hookName": "after all", - "hookId": "h4", + "hookId": "h3", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1082,20 +1116,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep fail with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1169,7 +1204,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "hookId": "h1", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1183,7 +1219,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1217,20 +1254,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1243,7 +1281,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "hookId": "h2", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1257,7 +1296,8 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1293,20 +1333,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1342,20 +1383,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1365,10 +1407,11 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "id": "r5", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1378,11 +1421,12 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "id": "r5", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1392,10 +1436,11 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "id": "r5", "title": "\"after all\" hook", "hookName": "after all", - "hookId": "h4", + "hookId": "h3", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1405,11 +1450,12 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with "id": "r5", "title": "\"after all\" hook", "hookName": "after all", - "hookId": "h4", + "hookId": "h3", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1446,20 +1492,21 @@ exports['src/cypress/runner tests finish with correct state mocha grep pass with }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1528,20 +1575,22 @@ exports['serialize state - hooks'] = { }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": 1, "afterFnDuration": 1 } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": 1, "afterFnDuration": 1 } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}", + "hooks": [] }, "r5": { "id": "r5", @@ -1560,7 +1609,9 @@ exports['serialize state - hooks'] = { "afterFnDuration": 1 } }, - "file": null + "file": null, + "invocationDetails": "{Object 8}", + "hooks": [] } }, "startTime": "1970-01-01T00:00:00.000Z", @@ -1653,7 +1704,8 @@ exports['src/cypress/runner mocha events simple single test #1'] = [ "title": "test 1", "body": "[body]", "type": "test", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1675,7 +1727,8 @@ exports['src/cypress/runner mocha events simple single test #1'] = [ "afterFnDuration": "match.number" } }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1697,7 +1750,8 @@ exports['src/cypress/runner mocha events simple single test #1'] = [ "afterFnDuration": "match.number" } }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1731,7 +1785,8 @@ exports['src/cypress/runner mocha events simple single test #1'] = [ "afterFnDuration": "match.number" } }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1794,7 +1849,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "hookId": "h1", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1808,7 +1864,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1843,13 +1900,14 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1862,7 +1920,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "hookId": "h2", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1876,7 +1935,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1913,13 +1973,14 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1956,13 +2017,14 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1972,10 +2034,11 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r3", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -1985,11 +2048,12 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r3", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2027,13 +2091,14 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2045,7 +2110,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "title": "test 2", "body": "[body]", "type": "test", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2059,7 +2125,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2073,7 +2140,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2103,13 +2171,14 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2139,13 +2208,14 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2155,11 +2225,12 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r4", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2169,11 +2240,12 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r4", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2204,13 +2276,14 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2222,7 +2295,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "title": "test 3", "body": "[body]", "type": "test", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2236,7 +2310,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2250,7 +2325,8 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2280,20 +2356,21 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2323,20 +2400,21 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2346,11 +2424,12 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r5", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2360,11 +2439,12 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r5", "title": "\"after each\" hook", "hookName": "after each", - "hookId": "h3", + "hookId": "h4", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2374,10 +2454,11 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r5", "title": "\"after all\" hook", "hookName": "after all", - "hookId": "h4", + "hookId": "h3", "body": "[body]", "type": "hook", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2387,11 +2468,12 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ "id": "r5", "title": "\"after all\" hook", "hookName": "after all", - "hookId": "h4", + "hookId": "h3", "body": "[body]", "type": "hook", "duration": "match.number", - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ @@ -2433,20 +2515,21 @@ exports['src/cypress/runner mocha events simple three tests #1'] = [ }, "after each": [ { - "hookId": "h3", + "hookId": "h4", "fnDuration": "match.number", "afterFnDuration": "match.number" } ], "after all": [ { - "hookId": "h4", + "hookId": "h3", "fnDuration": "match.number", "afterFnDuration": "match.number" } ] }, - "file": null + "file": null, + "invocationDetails": "{Object 8}" } ], [ diff --git a/packages/runner/cypress/fixtures/hook_spec.js b/packages/runner/cypress/fixtures/hook_spec.js new file mode 100644 index 000000000000..0062ce956e66 --- /dev/null +++ b/packages/runner/cypress/fixtures/hook_spec.js @@ -0,0 +1,47 @@ +describe('my test', () => { + before(() => { + cy.log('beforeHook 1') + }) + + beforeEach(() => { + cy.log('beforeEachHook 1') + }) + + it('tests 1', () => { + cy.log('testBody 1') + }) + + describe('nested suite', () => { + before(() => { + cy.log('beforeHook 2') + }) + + before(() => { + cy.log('beforeHook 3') + }) + + beforeEach(() => { + cy.log('beforeEachHook 2') + }) + + it('tests 2', () => { + cy.log('testBody 2') + }) + + afterEach(() => { + cy.log('afterEachHook 2') + }) + + after(() => { + cy.log('afterHook 2') + }) + }) + + afterEach(() => { + cy.log('afterEachHook 1') + }) + + after(() => { + cy.log('afterHook 1') + }) +}) diff --git a/packages/runner/cypress/integration/reporter.hooks.spec.js b/packages/runner/cypress/integration/reporter.hooks.spec.js new file mode 100644 index 000000000000..b5bf213b7c90 --- /dev/null +++ b/packages/runner/cypress/integration/reporter.hooks.spec.js @@ -0,0 +1,70 @@ +const helpers = require('../support/helpers') + +const { createCypress } = helpers +const { runIsolatedCypress } = createCypress() + +describe('hooks', function () { + beforeEach(function () { + this.editor = {} + + return runIsolatedCypress(`cypress/fixtures/hook_spec.js`, { + onBeforeRun: ({ win }) => { + this.win = win + + win.runnerWs.emit.withArgs('get:user:editor') + .yields({ + preferredOpener: this.editor, + }) + }, + }) + }) + + it('displays commands under correct hook', function () { + cy.contains('tests 1').click() + + cy.contains('before all').closest('.collapsible').should('contain', 'beforeHook 1') + cy.contains('before each').closest('.collapsible').should('contain', 'beforeEachHook 1') + cy.contains('test body').closest('.collapsible').should('contain', 'testBody 1') + cy.contains('after each').closest('.collapsible').should('contain', 'afterEachHook 1') + }) + + it('displays hooks without number when only one of type', function () { + cy.contains('tests 1').click() + + cy.contains('before all').should('not.contain', '(1)') + cy.contains('before each').should('not.contain', '(1)') + cy.contains('after each').should('not.contain', '(1)') + }) + + it('displays hooks separately with number when more than one of type', function () { + cy.contains('tests 2').click() + + cy.contains('before all (1)').closest('.collapsible').should('contain', 'beforeHook 2') + cy.contains('before all (2)').closest('.collapsible').should('contain', 'beforeHook 3') + cy.contains('before each (1)').closest('.collapsible').should('contain', 'beforeEachHook 1') + cy.contains('before each (2)').closest('.collapsible').should('contain', 'beforeEachHook 2') + cy.contains('test body').closest('.collapsible').should('contain', 'testBody 2') + cy.contains('after each (1)').closest('.collapsible').should('contain', 'afterEachHook 2') + cy.contains('after each (2)').closest('.collapsible').should('contain', 'afterEachHook 1') + cy.contains('after all (1)').closest('.collapsible').should('contain', 'afterHook 2') + cy.contains('after all (2)').closest('.collapsible').should('contain', 'afterHook 1') + }) + + it('creates open in IDE button', function () { + cy.contains('tests 1').click() + + cy.get('.hook-open-in-ide').should('have.length', 4) + }) + + it('properly opens file in IDE at hook', function () { + cy.contains('tests 1').click() + + cy.contains('Open in IDE').invoke('show').click().then(function () { + expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include('hook_spec.js') + // chrome sets the column to right before "before(" + // while firefox sets it right after "before(" + expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].column).to.be.eq(Cypress.browser.family === 'firefox' ? 10 : 3) + expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].line).to.be.eq(2) + }) + }) +}) diff --git a/packages/runner/cypress/support/helpers.js b/packages/runner/cypress/support/helpers.js index 26e178ac001f..a1d0d0c39f13 100644 --- a/packages/runner/cypress/support/helpers.js +++ b/packages/runner/cypress/support/helpers.js @@ -17,6 +17,7 @@ const eventCleanseMap = { tests: stringifyShort, commands: stringifyShort, err: stringifyShort, + invocationDetails: stringifyShort, body: '[body]', wallClockStartedAt: match.date, lifecycle: match.number, @@ -439,6 +440,7 @@ const cleanseRunStateMap = { 'err.stack': '[err stack]', sourceMappedStack: match.string, parsedStack: match.array, + invocationDetails: stringifyShort, } const shouldHaveTestResults = (expPassed, expFailed) => { diff --git a/packages/server/__snapshots__/5_spec_isolation_spec.js b/packages/server/__snapshots__/5_spec_isolation_spec.js index 63d47a449962..a756c1dbb073 100644 --- a/packages/server/__snapshots__/5_spec_isolation_spec.js +++ b/packages/server/__snapshots__/5_spec_isolation_spec.js @@ -422,7 +422,7 @@ exports['e2e spec isolation fails'] = { "body": "function () {\n return cy.wait(200);\n }" }, { - "hookId": "h4", + "hookId": "h5", "hookName": "after each", "title": [ "\"after each\" hook" @@ -430,7 +430,7 @@ exports['e2e spec isolation fails'] = { "body": "function () {\n return cy.wait(200);\n }" }, { - "hookId": "h5", + "hookId": "h4", "hookName": "after all", "title": [ "\"after all\" hook" @@ -476,7 +476,7 @@ exports['e2e spec isolation fails'] = { }, "after each": [ { - "hookId": "h4", + "hookId": "h5", "fnDuration": 400, "afterFnDuration": 200 } @@ -512,7 +512,7 @@ exports['e2e spec isolation fails'] = { }, "after each": [ { - "hookId": "h4", + "hookId": "h5", "fnDuration": 400, "afterFnDuration": 200 } @@ -548,14 +548,14 @@ exports['e2e spec isolation fails'] = { }, "after each": [ { - "hookId": "h4", + "hookId": "h5", "fnDuration": 400, "afterFnDuration": 200 } ], "after all": [ { - "hookId": "h5", + "hookId": "h4", "fnDuration": 400, "afterFnDuration": 200 }