diff --git a/cli/schema/cypress.schema.json b/cli/schema/cypress.schema.json
index dd9f6b8fcb..9298105f65 100644
--- a/cli/schema/cypress.schema.json
+++ b/cli/schema/cypress.schema.json
@@ -243,7 +243,7 @@
"experimentalRunEvents": {
"type": "boolean",
"default": false,
- "description": "Allows listening to the `before:spec` event in the plugins file."
+ "description": "Allows listening to the `before:spec` and `after:spec` events in the plugins file."
},
"experimentalSourceRewriting": {
"type": "boolean",
diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts
index ee0ab096a2..e1075c0009 100644
--- a/cli/types/cypress.d.ts
+++ b/cli/types/cypress.d.ts
@@ -1,3 +1,5 @@
+///
+
declare namespace Cypress {
type FileContents = string | any[] | object
type HistoryDirection = 'back' | 'forward'
@@ -116,6 +118,17 @@ declare namespace Cypress {
*/
type CypressSpecType = 'integration' | 'component'
+ /**
+ * A Cypress spec.
+ */
+ interface Spec {
+ name: string // "config_passing_spec.js"
+ relative: string // "cypress/integration/config_passing_spec.js" or "__all" if clicked all specs button
+ absolute: string // "/Users/janelane/app/cypress/integration/config_passing_spec.js"
+ specFilter?: string // optional spec filter used by the user
+ specType?: CypressSpecType
+ }
+
/**
* Window type for Application Under Test(AUT)
*/
@@ -227,23 +240,17 @@ declare namespace Cypress {
/**
* Currently executing spec file.
* @example
- ```
- Cypress.spec
- // {
- // name: "config_passing_spec.coffee",
- // relative: "cypress/integration/config_passing_spec.coffee",
- // absolute: "/users/smith/projects/web/cypress/integration/config_passing_spec.coffee"
- // specType: "integration"
- // }
- ```
+ * ```
+ * Cypress.spec
+ * // {
+ * // name: "config_passing_spec.coffee",
+ * // relative: "cypress/integration/config_passing_spec.coffee",
+ * // absolute: "/users/smith/projects/web/cypress/integration/config_passing_spec.coffee"
+ * // specType: "integration"
+ * // }
+ * ```
*/
- spec: {
- name: string // "config_passing_spec.coffee"
- relative: string // "cypress/integration/config_passing_spec.coffee" or "__all" if clicked all specs button
- absolute: string
- specFilter?: string // optional spec filter used by the user
- specType?: CypressSpecType
- }
+ spec: Spec
/**
* Information about the browser currently running the tests
@@ -2570,7 +2577,7 @@ declare namespace Cypress {
*/
firefoxGcInterval: Nullable, openMode: Nullable }>
/**
- * Allows listening to the `before:spec` event in the plugins file.
+ * Allows listening to the `before:spec` and `after:spec` events in the plugins file.
* @default false
*/
experimentalRunEvents: boolean
@@ -2597,7 +2604,7 @@ declare namespace Cypress {
includeShadowDom: boolean
}
- interface TestConfigOverrides extends Partial> {
+ interface TestConfigOverrides extends Partial> {
browser?: IsBrowserMatcher | IsBrowserMatcher[]
}
@@ -4993,8 +5000,10 @@ declare namespace Cypress {
}
interface PluginEvents {
- (action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: BrowserLaunchOptions) => void | BrowserLaunchOptions | Promise): void
(action: 'after:screenshot', fn: (details: ScreenshotDetails) => void | AfterScreenshotReturnObject | Promise): void
+ (action: 'after:spec', fn: (spec: Spec, results: CypressCommandLine.RunResult) => void | Promise): void
+ (action: 'before:spec', fn: (spec: Spec) => void | Promise): void
+ (action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: BrowserLaunchOptions) => void | BrowserLaunchOptions | Promise): void
(action: 'file:preprocessor', fn: (file: FileObject) => string | Promise): void
(action: 'task', tasks: Tasks): void
}
diff --git a/packages/server/__snapshots__/4_plugin_run_events_spec.ts.js b/packages/server/__snapshots__/4_plugin_run_events_spec.ts.js
index 31c9760c82..781e5936e9 100644
--- a/packages/server/__snapshots__/4_plugin_run_events_spec.ts.js
+++ b/packages/server/__snapshots__/4_plugin_run_events_spec.ts.js
@@ -1,4 +1,13 @@
-exports['e2e plugin run events / sends server events'] = `
+exports['e2e plugin run events / fails if experimentalRunEvents is not enabled'] = `
+The following validation error was thrown by your plugins file (\`/foo/bar/.projects/plugin-run-events/cypress/plugins/index.js\`).
+
+ Error: The \`before:spec\` event requires the experimentalRunEvents flag to be enabled.
+
+To enable it, set \`"experimentalRunEvents": true\` in your cypress.json
+ [stack trace lines]
+`
+
+exports['e2e plugin run events / sends events'] = `
====================================================================================================
@@ -39,6 +48,8 @@ before:spec is awaited
│ Spec Ran: run_events_spec_1.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
+spec:end: cypress/integration/run_events_spec_1.js { tests: 1, passes: 1, failures: 0 }
+after:spec is awaited
────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -66,6 +77,8 @@ before:spec is awaited
│ Spec Ran: run_events_spec_2.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
+spec:end: cypress/integration/run_events_spec_2.js { tests: 1, passes: 1, failures: 0 }
+after:spec is awaited
====================================================================================================
@@ -83,7 +96,7 @@ before:spec is awaited
`
-exports['e2e plugin run events / fails run if server event handler throws'] = `
+exports['e2e plugin run events / fails run if event handler throws'] = `
====================================================================================================
@@ -110,12 +123,3 @@ Error: error thrown in before:spec
`
-
-exports['e2e plugin run events / fails if experimentalRunEvents is not enabled'] = `
-The following validation error was thrown by your plugins file (\`/foo/bar/.projects/plugin-run-events/cypress/plugins/index.js\`).
-
- Error: The \`before:spec\` event requires the experimentalRunEvents flag to be enabled.
-
-To enable it, set \`"experimentalRunEvents": true\` in your cypress.json
- [stack trace lines]
-`
diff --git a/packages/server/lib/experiments.ts b/packages/server/lib/experiments.ts
index 895e09e339..eda02b8f1c 100644
--- a/packages/server/lib/experiments.ts
+++ b/packages/server/lib/experiments.ts
@@ -53,7 +53,7 @@ interface StringValues {
const _summaries: StringValues = {
experimentalComponentTesting: 'Framework-specific component testing, uses `componentFolder` to load component specs.',
experimentalFetchPolyfill: 'Polyfills `window.fetch` to enable Network spying and stubbing.',
- experimentalRunEvents: 'Allows listening to the `before:spec` event in the plugins file.',
+ experimentalRunEvents: 'Allows listening to the `before:spec` and `after:spec` events in the plugins file.',
experimentalSourceRewriting: 'Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm.',
}
diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js
index aea3f3a4e7..ec7c8fc078 100644
--- a/packages/server/lib/modes/run.js
+++ b/packages/server/lib/modes/run.js
@@ -1080,7 +1080,7 @@ module.exports = {
},
waitForTestsToFinishRunning (options = {}) {
- const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated, quiet } = options
+ const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated, quiet, config } = options
// https://github.com/cypress-io/cypress/issues/2370
// delay 1 second if we're recording a video to give
@@ -1090,8 +1090,8 @@ module.exports = {
return this.listenForProjectEnd(project, exit)
.delay(delay)
- .then(async (obj) => {
- _.defaults(obj, {
+ .then(async (results) => {
+ _.defaults(results, {
error: null,
hooks: null,
tests: null,
@@ -1101,27 +1101,23 @@ module.exports = {
})
if (startedVideoCapture) {
- obj.video = videoName
+ results.video = videoName
}
if (screenshots) {
- obj.screenshots = screenshots
+ results.screenshots = screenshots
}
- obj.spec = spec
-
- const finish = () => {
- return obj
- }
+ results.spec = spec
if (!quiet) {
- this.displayResults(obj, estimated)
+ this.displayResults(results, estimated)
if (screenshots && screenshots.length) {
this.displayScreenshots(screenshots)
}
}
- const { tests, stats } = obj
+ const { tests, stats } = results
const attempts = _.flatMap(tests, (test) => test.attempts)
@@ -1137,7 +1133,7 @@ module.exports = {
// or if we have any failures and have started the video
const suv = Boolean(videoUploadOnPasses === true || (startedVideoCapture && hasFailingTests))
- obj.shouldUploadVideo = suv
+ results.shouldUploadVideo = suv
let videoCaptureFailed = false
@@ -1147,6 +1143,8 @@ module.exports = {
.catch(warnVideoRecordingFailed)
}
+ await runEvents.execute('after:spec', config, _.cloneDeep(spec), _.cloneDeep(results))
+
// always close the browser now as opposed to letting
// it exit naturally with the parent process due to
// electron bug in windows
@@ -1154,7 +1152,7 @@ module.exports = {
await openProject.closeBrowser()
if (endVideoCapture && !videoCaptureFailed) {
- const ffmpegChaptersConfig = videoCapture.generateFfmpegChaptersConfig(obj.tests)
+ const ffmpegChaptersConfig = videoCapture.generateFfmpegChaptersConfig(results.tests)
await this.postProcessRecording(
videoName,
@@ -1167,7 +1165,7 @@ module.exports = {
.catch(warnVideoRecordingFailed)
}
- return finish()
+ return results
})
},
@@ -1322,7 +1320,7 @@ module.exports = {
const screenshots = []
- return runEvents.execute('before:spec', config, spec)
+ return runEvents.execute('before:spec', config, _.cloneDeep(spec))
.then(() => {
// we know we're done running headlessly
// when the renderer has connected and
@@ -1340,6 +1338,7 @@ module.exports = {
return Promise.props({
results: this.waitForTestsToFinishRunning({
spec,
+ config,
project,
estimated,
screenshots,
diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js
index 797253573d..217116bf0d 100644
--- a/packages/server/lib/plugins/child/run_plugins.js
+++ b/packages/server/lib/plugins/child/run_plugins.js
@@ -111,6 +111,9 @@ const execute = (ipc, event, ids, args = []) => {
'after:screenshot' () {
util.wrapChildPromise(ipc, invoke, ids, args)
},
+ 'after:spec' () {
+ util.wrapChildPromise(ipc, invoke, ids, args)
+ },
'before:browser:launch' () {
browserLaunch.wrap(ipc, invoke, ids, args)
},
diff --git a/packages/server/lib/plugins/child/validate_event.js b/packages/server/lib/plugins/child/validate_event.js
index d360b37760..bc693ae579 100644
--- a/packages/server/lib/plugins/child/validate_event.js
+++ b/packages/server/lib/plugins/child/validate_event.js
@@ -34,8 +34,13 @@ const eventValidators = {
'_get:task:body': isFunction,
}
+const runEvents = {
+ 'after:spec': true,
+ 'before:spec': true,
+}
+
const validateEvent = (event, handler, config) => {
- if (event === 'before:spec') {
+ if (runEvents[event]) {
return isValidRunEvent(event, handler, config)
}
diff --git a/packages/server/lib/plugins/run_events.js b/packages/server/lib/plugins/run_events.js
index aef0a3d15e..aeda3c7ffb 100644
--- a/packages/server/lib/plugins/run_events.js
+++ b/packages/server/lib/plugins/run_events.js
@@ -4,7 +4,7 @@ const errors = require('../errors')
const plugins = require('../plugins')
module.exports = {
- execute: Promise.method((eventName, config, ...args) => {
+ execute: Promise.method((eventName, config = {}, ...args) => {
if (!config.experimentalRunEvents) return
if (!plugins.has(eventName)) return
diff --git a/packages/server/test/e2e/4_plugin_run_events_spec.ts b/packages/server/test/e2e/4_plugin_run_events_spec.ts
index 17288a5b2d..c8536cb3b5 100644
--- a/packages/server/test/e2e/4_plugin_run_events_spec.ts
+++ b/packages/server/test/e2e/4_plugin_run_events_spec.ts
@@ -4,7 +4,7 @@ import Fixtures from '../support/helpers/fixtures'
describe('e2e plugin run events', () => {
e2e.setup()
- e2e.it('sends server events', {
+ e2e.it('sends events', {
browser: 'electron',
project: Fixtures.projectPath('plugin-run-events'),
spec: '*',
@@ -26,7 +26,7 @@ describe('e2e plugin run events', () => {
},
})
- e2e.it('fails run if server event handler throws', {
+ e2e.it('fails run if event handler throws', {
browser: 'electron',
project: Fixtures.projectPath('plugin-run-event-throws'),
spec: '*',
diff --git a/packages/server/test/support/fixtures/projects/plugin-run-events/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/plugin-run-events/cypress/plugins/index.js
index a26dc2eaf9..f3f8d23884 100644
--- a/packages/server/test/support/fixtures/projects/plugin-run-events/cypress/plugins/index.js
+++ b/packages/server/test/support/fixtures/projects/plugin-run-events/cypress/plugins/index.js
@@ -9,4 +9,15 @@ module.exports = (on) => {
return console.log('before:spec is awaited')
})
})
+
+ on('after:spec', (spec, results) => {
+ const { stats } = results
+ const { tests, passes, failures } = stats
+
+ console.log('spec:end:', spec.relative, { tests, passes, failures })
+
+ return Promise.delay(10).then(() => {
+ return console.log('after:spec is awaited')
+ })
+ })
}
diff --git a/packages/server/test/unit/plugins/child/validate_event_spec.js b/packages/server/test/unit/plugins/child/validate_event_spec.js
index b6e08d675e..ff418a3a12 100644
--- a/packages/server/test/unit/plugins/child/validate_event_spec.js
+++ b/packages/server/test/unit/plugins/child/validate_event_spec.js
@@ -68,26 +68,33 @@ The following are valid events:
})
describe('run events', () => {
- it('returns error when before:spec event is registered without experimentalRunEvents flag enabled', () => {
- const { isValid, error } = validateEvent('before:spec', {}, { experimentalRunEvents: false })
+ const runEvents = [
+ 'before:spec',
+ 'after:spec',
+ ]
- expect(isValid).to.be.false
- expect(error.message).to.equal(`The \`before:spec\` event requires the experimentalRunEvents flag to be enabled.
+ _.each(runEvents, (event) => {
+ it(`returns error when ${event} event is registed without experimentalRunEvents flag enabled`, () => {
+ const { isValid, error } = validateEvent(event, {}, { experimentalRunEvents: false })
+
+ expect(isValid).to.be.false
+ expect(error.message).to.equal(`The \`${event}\` event requires the experimentalRunEvents flag to be enabled.
To enable it, set \`"experimentalRunEvents": true\` in your cypress.json`)
- })
+ })
- it('returns error when event handler of before:spec is not a function', () => {
- const { isValid, error } = validateEvent('before:spec', 'invalid type', { experimentalRunEvents: true })
+ it(`returns error when event handler of ${event} is not a function`, () => {
+ const { isValid, error } = validateEvent(event, 'invalid type', { experimentalRunEvents: true })
- expect(isValid).to.be.false
- expect(error.message).to.equal(`The handler for the event \`before:spec\` must be a function`)
- })
+ expect(isValid).to.be.false
+ expect(error.message).to.equal(`The handler for the event \`${event}\` must be a function`)
+ })
- it('returns success when event handler of before:spec is a function', () => {
- const { isValid } = validateEvent('before:spec', () => {}, { experimentalRunEvents: true })
+ it(`returns success when event handler of ${event} is a function`, () => {
+ const { isValid } = validateEvent(event, () => {}, { experimentalRunEvents: true })
- expect(isValid).to.be.true
+ expect(isValid).to.be.true
+ })
})
})
})