mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-29 10:29:27 -06:00
Split hooks and open hooks in ide from reporter (#7821)
* Add invocation details to hooks * Add hooks to emitted root runnable * Hook details model * Progress on hook models * Add commands to hook * Fix display of hooks * Display hooks in correct order * (More) efficiently reorder hooks and display split numbers * Open hook in IDE functionality * Properly style button to open in IDE * Add ability to open test body in IDE * Fix hooks specs * Runnables store tests * Test model unit tests * HookDetails -> HookProps * Fix reporter integration tests * Fix issue with after hook * Update runner mocha tests * Remove driver integration test that is no longer needed * Update snapshot for server tests * Add reporter integration tests for hooks * Fix driver querying test * Add runner test for hooks * Update reporter hook tests to check for before all * Fix before/after hook counts * Fix runner test * Fix issue with stack trace in ff and clean up mocha override * Just incase someone names their test or support file common.js Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com>
This commit is contained in:
@@ -502,20 +502,18 @@ describe('src/cy/commands/querying', () => {
|
||||
})
|
||||
|
||||
describe('.log', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
this.logs = []
|
||||
beforeEach(function () {
|
||||
this.logs = []
|
||||
|
||||
cy.on('log:added', (attrs, log) => {
|
||||
if (attrs.name === 'root') {
|
||||
this.lastLog = log
|
||||
cy.on('log:added', (attrs, log) => {
|
||||
if (attrs.name === 'root') {
|
||||
this.lastLog = log
|
||||
|
||||
this.logs.push(log)
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
this.logs.push(log)
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
it('can turn off logging', () => {
|
||||
|
||||
@@ -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', () => {})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"id": "r1",
|
||||
"title": "",
|
||||
"root": true,
|
||||
"hooks": [],
|
||||
"suites": [],
|
||||
"tests": [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
199
packages/reporter/cypress/fixtures/runnables_hooks.json
Normal file
199
packages/reporter/cypress/fixtures/runnables_hooks.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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: {},
|
||||
|
||||
113
packages/reporter/cypress/integration/hooks_spec.ts
Normal file
113
packages/reporter/cypress/integration/hooks_spec.ts
Normal file
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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<Command> = []
|
||||
@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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
.hook-open-in-ide {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:focus {
|
||||
border-bottom: 1px dotted #959595;
|
||||
color: #333;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
> .collapsible-header-inner:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Props> {
|
||||
|
||||
return (
|
||||
<div className='test-err-code-frame'>
|
||||
<FileOpener className="runnable-err-file-path" fileDetails={this.props.codeFrame} />
|
||||
<FileNameOpener className="runnable-err-file-path" fileDetails={this.props.codeFrame} />
|
||||
<pre ref='codeFrame' data-line={highlightLine}>
|
||||
<code className={`language-${language || 'text'}`}>{frame}</code>
|
||||
</pre>
|
||||
|
||||
@@ -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 = (
|
||||
<FileOpener key={key} className="runnable-err-file-path" fileDetails={stackLine} />
|
||||
<FileNameOpener key={key} className="runnable-err-file-path" fileDetails={stackLine} />
|
||||
)
|
||||
|
||||
return makeLine(key, [whitespace, `at ${fn} (`, link, ')'])
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<CommandModel> = []
|
||||
@observable failed = false
|
||||
|
||||
private _aliasesWithDuplicatesCache: Array<Alias> | 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 () {
|
||||
|
||||
@@ -11,8 +11,8 @@ const commandModel = () => {
|
||||
|
||||
const hookModel = (props?: Partial<HookModel>) => {
|
||||
return _.extend<HookModel>({
|
||||
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<HookModel>) => {
|
||||
const model = (props?: Partial<HooksModel>) => {
|
||||
return _.extend<HooksModel>({
|
||||
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('<Hooks />', () => {
|
||||
expect(component.find(Hook).length).to.equal(3)
|
||||
})
|
||||
|
||||
it('renders a number when there are multiple hooks of the same name', () => {
|
||||
const component = shallow(<Hooks model={model()} />)
|
||||
|
||||
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(<Hooks model={model({
|
||||
hooks: [hookModel()],
|
||||
hookCount: {
|
||||
'before all': 0,
|
||||
'before each': 1,
|
||||
'after all': 0,
|
||||
'after each': 0,
|
||||
'test body': 0,
|
||||
},
|
||||
})} />)
|
||||
|
||||
expect(component.find(Hook).first()).to.have.prop('showNumber', false)
|
||||
})
|
||||
|
||||
context('<Hook />', () => {
|
||||
it('renders without hook-failed class when not failed', () => {
|
||||
const component = shallow(<Hook model={hookModel()} />)
|
||||
const component = shallow(<Hook model={hookModel()} showNumber={false} />)
|
||||
|
||||
expect(component).not.to.have.className('hook-failed')
|
||||
})
|
||||
|
||||
it('renders with hook-failed class when failed', () => {
|
||||
const component = shallow(<Hook model={hookModel({ failed: true })} />)
|
||||
const component = shallow(<Hook model={hookModel({ failed: true })} showNumber={false} />)
|
||||
|
||||
expect(component).to.have.className('hook-failed')
|
||||
})
|
||||
|
||||
it('renders Collapsible with hook header', () => {
|
||||
const component = shallow(<Hook model={hookModel()} />)
|
||||
const component = shallow(<Hook model={hookModel()} showNumber={false} />)
|
||||
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(<Hook model={hookModel()} />)
|
||||
const component = shallow(<Hook model={hookModel()} showNumber={false} />)
|
||||
|
||||
expect(component.find('Collapsible').prop('isOpen')).to.be.true
|
||||
})
|
||||
|
||||
it('renders command for each in model', () => {
|
||||
const component = shallow(<Hook model={hookModel()} />)
|
||||
const component = shallow(<Hook model={hookModel()} showNumber={false} />)
|
||||
|
||||
expect(component.find('Command').length).to.equal(2)
|
||||
})
|
||||
@@ -76,5 +104,11 @@ describe('<Hooks />', () => {
|
||||
|
||||
expect(component.find('.hook-failed-message')).to.have.text('(failed)')
|
||||
})
|
||||
|
||||
it('renders the number', () => {
|
||||
const component = shallow(<HookHeader name='before' number={1} />)
|
||||
|
||||
expect(component.text()).to.contain('before (1)')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) => (
|
||||
<span>
|
||||
{name === 'test' ? 'test body' : name} <span className='hook-failed-message'>(failed)</span>
|
||||
const HookHeader = ({ name, number }: HookHeaderProps) => (
|
||||
<span className='hook-name'>
|
||||
{name} {number && `(${number})`} <span className='hook-failed-message'>(failed)</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
export interface HookOpenInIDEProps {
|
||||
invocationDetails: FileDetails
|
||||
}
|
||||
|
||||
const HookOpenInIDE = ({ invocationDetails }: HookOpenInIDEProps) => (
|
||||
<FileOpener fileDetails={invocationDetails} className='hook-open-in-ide'>
|
||||
<i className="fas fa-external-link-alt fa-sm" /> <span>Open in IDE</span>
|
||||
</FileOpener>
|
||||
)
|
||||
|
||||
export interface HookProps {
|
||||
model: HookModel
|
||||
showNumber: boolean
|
||||
}
|
||||
|
||||
const Hook = observer(({ model }: HookProps) => (
|
||||
const Hook = observer(({ model, showNumber }: HookProps) => (
|
||||
<li className={cs('hook-item', { 'hook-failed': model.failed })}>
|
||||
<Collapsible
|
||||
header={<HookHeader name={model.name} />}
|
||||
headerClass='hook-name'
|
||||
header={<HookHeader name={model.hookName} number={showNumber ? model.hookNumber : undefined} />}
|
||||
headerClass='hook-header'
|
||||
headerExtras={model.invocationDetails && <HookOpenInIDE invocationDetails={model.invocationDetails} />}
|
||||
isOpen={true}
|
||||
>
|
||||
<ul className='commands-container'>
|
||||
@@ -36,6 +52,7 @@ const Hook = observer(({ model }: HookProps) => (
|
||||
|
||||
export interface HooksModel {
|
||||
hooks: Array<HookModel>
|
||||
hookCount: { [name in HookName]: number }
|
||||
}
|
||||
|
||||
export interface HooksProps {
|
||||
@@ -44,7 +61,7 @@ export interface HooksProps {
|
||||
|
||||
const Hooks = observer(({ model }: HooksProps) => (
|
||||
<ul className='hooks-container'>
|
||||
{_.map(model.hooks, (hook) => <Hook key={hook.id} model={hook} />)}
|
||||
{_.map(model.hooks, (hook) => hook.commands.length ? <Hook key={hook.hookId} model={hook} showNumber={model.hookCount[hook.hookName] > 1} /> : undefined)}
|
||||
</ul>
|
||||
))
|
||||
|
||||
|
||||
28
packages/reporter/src/lib/file-name-opener.tsx
Normal file
28
packages/reporter/src/lib/file-name-opener.tsx
Normal file
@@ -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 (
|
||||
<Tooltip title={'Open in IDE'} wrapperClassName={props.className} className='cy-tooltip'>
|
||||
<span>
|
||||
<FileOpener fileDetails={props.fileDetails}>
|
||||
{originalFile}{!!line && `:${line}`}{!!column && `:${column}`}
|
||||
</FileOpener>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
export default FileNameOpener
|
||||
@@ -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 (
|
||||
<Tooltip title={'Open in IDE'} wrapperClassName={props.className} className='cy-tooltip'>
|
||||
<span>
|
||||
<Opener
|
||||
openFile={openFile}
|
||||
getUserEditor={getUserEditor}
|
||||
setUserEditor={setUserEditor}
|
||||
fileDetails={props.fileDetails}
|
||||
>
|
||||
{originalFile}{!!line && `:${line}`}{!!column && `:${column}`}
|
||||
</Opener>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
const FileOpener = observer(({ fileDetails, children, className }: Props) => (
|
||||
<Opener
|
||||
openFile={openFile}
|
||||
getUserEditor={getUserEditor}
|
||||
setUserEditor={setUserEditor}
|
||||
fileDetails={fileDetails}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Opener>
|
||||
))
|
||||
|
||||
export default FileOpener
|
||||
|
||||
@@ -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) => <div className="runnable-header">{children}</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ class RunnableHeader extends Component<RunnableHeaderProps> {
|
||||
}
|
||||
|
||||
return renderRunnableHeader(
|
||||
<FileOpener fileDetails={fileDetails} />,
|
||||
<FileNameOpener fileDetails={fileDetails} />,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { observable } from 'mobx'
|
||||
import { HookProps } from '../hooks/hook-model'
|
||||
|
||||
export interface RunnableProps {
|
||||
id: number
|
||||
title?: string
|
||||
hooks: Array<HookProps>
|
||||
}
|
||||
|
||||
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<HookProps> = []
|
||||
|
||||
constructor (props: RunnableProps, level: number) {
|
||||
this.id = props.id
|
||||
this.title = props.title
|
||||
this.level = level
|
||||
this.hooks = props.hooks
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TestProps>, suites: Array<SuiteProps>) => {
|
||||
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')
|
||||
})
|
||||
|
||||
@@ -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<TestModel | SuiteModel>
|
||||
type Log = AgentModel | CommandModel | RouteModel
|
||||
|
||||
export interface RootRunnable {
|
||||
hooks?: Array<HookProps>
|
||||
tests?: Array<TestProps>
|
||||
suites?: Array<SuiteProps>
|
||||
}
|
||||
@@ -73,18 +75,20 @@ class RunnablesStore {
|
||||
}
|
||||
|
||||
_createRunnableChildren (runnableProps: RootRunnable, level: number) {
|
||||
return this._createRunnables<TestProps>('test', runnableProps.tests || [], level).concat(
|
||||
this._createRunnables<SuiteProps>('suite', runnableProps.suites || [], level),
|
||||
return this._createRunnables<TestProps>('test', runnableProps.tests || [], runnableProps.hooks || [], level).concat(
|
||||
this._createRunnables<SuiteProps>('suite', runnableProps.suites || [], runnableProps.hooks || [], level),
|
||||
)
|
||||
}
|
||||
|
||||
_createRunnables<T> (type: RunnableType, runnables: Array<TestOrSuite<T>>, level: number) {
|
||||
_createRunnables<T> (type: RunnableType, runnables: Array<TestOrSuite<T>>, hooks: Array<HookProps>, level: number) {
|
||||
return _.map(runnables, (runnableProps) => {
|
||||
return this._createRunnable(type, runnableProps, level)
|
||||
return this._createRunnable(type, runnableProps, hooks, level)
|
||||
})
|
||||
}
|
||||
|
||||
_createRunnable<T> (type: RunnableType, props: TestOrSuite<T>, level: number) {
|
||||
_createRunnable<T> (type: RunnableType, props: TestOrSuite<T>, hooks: Array<HookProps>, 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
|
||||
|
||||
@@ -2,7 +2,7 @@ import Suite from './suite-model'
|
||||
import TestModel from '../test/test-model'
|
||||
|
||||
const suiteWithChildren = (children: Array<Partial<TestModel>>) => {
|
||||
const suite = new Suite({ id: 1, title: '' }, 0)
|
||||
const suite = new Suite({ id: 1, title: '', hooks: [] }, 0)
|
||||
|
||||
suite.children = children as Array<TestModel>
|
||||
|
||||
|
||||
@@ -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<Command> = (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)
|
||||
expect(test.hooks[1].commands.length).to.equal(0)
|
||||
expect(test.hooks[2].commands.length).to.equal(0)
|
||||
|
||||
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('adds the command to an existing hook if it already exists', () => {
|
||||
const test = new TestModel({} as TestProps, 0)
|
||||
const command: Partial<Command> = { isMatchingEvent: () => {
|
||||
return false
|
||||
} }
|
||||
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(command as Command, 'some hook')
|
||||
|
||||
expect(test.hooks.length).to.equal(1)
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<AgentProps>
|
||||
commands?: Array<CommandProps>
|
||||
routes?: Array<RouteProps>
|
||||
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<Route> = []
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const model = (props?: Partial<TestModel>) => {
|
||||
return _.extend<TestModel>({
|
||||
agents: [],
|
||||
commands: [],
|
||||
hooks: [],
|
||||
err: {},
|
||||
id: 't1',
|
||||
isActive: true,
|
||||
@@ -112,13 +113,13 @@ describe('<Test />', () => {
|
||||
})
|
||||
|
||||
it('renders <Hooks /> if there are commands', () => {
|
||||
const component = mount(<Test model={model({ commands: [{ id: 1 }], state: 'failed' } as TestModel)} />)
|
||||
const component = shallow(<Test model={model({ commands: [{ id: 1, hookId: 'h1' }], hooks: [{ hookId: 'h1' }], state: 'failed' } as TestModel)} />)
|
||||
|
||||
expect(component.find(Hooks)).to.exist
|
||||
})
|
||||
|
||||
it('renders <NoCommands /> is no commands', () => {
|
||||
const component = mount(<Test model={model({ state: 'failed' })} />)
|
||||
const component = shallow(<Test model={model({ state: 'failed' })} />)
|
||||
|
||||
expect(component.find(NoCommands)).to.exist
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
47
packages/runner/cypress/fixtures/hook_spec.js
Normal file
47
packages/runner/cypress/fixtures/hook_spec.js
Normal file
@@ -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')
|
||||
})
|
||||
})
|
||||
70
packages/runner/cypress/integration/reporter.hooks.spec.js
Normal file
70
packages/runner/cypress/integration/reporter.hooks.spec.js
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user