From beb411f3f4c0791e67b7f78f0f023bcac7633f07 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Thu, 2 Feb 2023 07:17:25 -0700 Subject: [PATCH] fix: `test:after:run` passes the `runnable` (#25678) --- cli/CHANGELOG.md | 4 + cli/types/cypress.d.ts | 4 +- cli/types/tests/actions.ts | 11 +- packages/app/src/runner/event-manager.ts | 15 +- .../driver/cypress/e2e/cypress/events.cy.ts | 205 ++++++++++++++++++ packages/driver/src/cypress.ts | 2 +- 6 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 packages/driver/cypress/e2e/cypress/events.cy.ts diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 15c3204b027f..19e16dbc0239 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,10 @@ _Released 02/10/2023 (PENDING)_ +**Bugfixes:** + +- Fixed a regression from Cypress [12.5.0](https://docs.cypress.io/guides/references/changelog#12-5-0) where the `runnable` was not included in the `test:after:run` event. Fixes [#25663](https://github.com/cypress-io/cypress/issues/25663). + **Dependency Updates:** - Upgraded [`simple-git`](https://github.com/steveukx/git-js) from `3.15.0` to `3.16.0` to address this [security vulnerability](https://github.com/advisories/GHSA-9p95-fxvg-qgq2) where Remote Code Execution (RCE) via the clone(), pull(), push() and listRemote() methods due to improper input sanitization was possible. Addressed in [#25603](https://github.com/cypress-io/cypress/pull/25603). diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 6aa84eb495a8..3dfaab987170 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -5976,14 +5976,14 @@ declare namespace Cypress { * Useful to see how internal cypress commands utilize the {% url 'Cypress.log()' cypress-log %} API. * @see https://on.cypress.io/catalog-of-events#App-Events */ - (action: 'log:added', fn: (log: any, interactive: boolean) => void): Cypress + (action: 'log:added', fn: (attributes: ObjectLike, log: any) => void): Cypress /** * Fires whenever a command's attributes changes. * This event is debounced to prevent it from firing too quickly and too often. * Useful to see how internal cypress commands utilize the {% url 'Cypress.log()' cypress-log %} API. * @see https://on.cypress.io/catalog-of-events#App-Events */ - (action: 'log:changed', fn: (log: any, interactive: boolean) => void): Cypress + (action: 'log:changed', fn: (attributes: ObjectLike, log: any) => void): Cypress /** * Fires before the test and all **before** and **beforeEach** hooks run. * @see https://on.cypress.io/catalog-of-events#App-Events diff --git a/cli/types/tests/actions.ts b/cli/types/tests/actions.ts index 6ebf7799a69f..af5563597d23 100644 --- a/cli/types/tests/actions.ts +++ b/cli/types/tests/actions.ts @@ -61,11 +61,13 @@ Cypress.on('command:retry', (command) => { command // $ExpectType CommandQueue }) -Cypress.on('log:added', (log, interactive: boolean) => { +Cypress.on('log:added', (attributes, log) => { + attributes // $ExpectType ObjectLike log // $ExpectTyped any }) -Cypress.on('log:changed', (log, interactive: boolean) => { +Cypress.on('log:changed', (attributes, log) => { + attributes // $ExpectType ObjectLike log // $ExpectTyped any }) @@ -74,6 +76,11 @@ Cypress.on('test:before:run', (attributes , test) => { test // $ExpectType Test }) +Cypress.on('test:before:run:async', (attributes , test) => { + attributes // $ExpectType ObjectLike + test // $ExpectType Test +}) + Cypress.on('test:after:run', (attributes , test) => { attributes // $ExpectType ObjectLike test // $ExpectType Test diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 28f3324fc512..223cf6310e73 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -38,7 +38,6 @@ interface AddGlobalListenerOptions { const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ') const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ') -const driverTestEvents = 'test:before:run:async test:after:run'.split(' ') const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ') const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:cookies'.split(' ') @@ -536,12 +535,6 @@ export class EventManager { Cypress.on('after:screenshot', handleAfterScreenshot) - driverTestEvents.forEach((event) => { - Cypress.on(event, (test, cb) => { - this.reporterBus.emit(event, test, cb) - }) - }) - driverToLocalAndReporterEvents.forEach((event) => { Cypress.on(event, (...args) => { this.localBus.emit(event, ...args) @@ -549,6 +542,14 @@ export class EventManager { }) }) + Cypress.on('test:before:run:async', (test, _runnable) => { + this.reporterBus.emit('test:before:run:async', test) + }) + + Cypress.on('test:after:run', (test, _runnable) => { + this.reporterBus.emit('test:after:run', test, Cypress.config('isInteractive')) + }) + Cypress.on('run:start', async () => { if (Cypress.config('experimentalMemoryManagement') && Cypress.isBrowser({ family: 'chromium' })) { await Cypress.backend('start:memory:profiling', Cypress.config('spec')) diff --git a/packages/driver/cypress/e2e/cypress/events.cy.ts b/packages/driver/cypress/e2e/cypress/events.cy.ts new file mode 100644 index 000000000000..e9aca8bfb1bd --- /dev/null +++ b/packages/driver/cypress/e2e/cypress/events.cy.ts @@ -0,0 +1,205 @@ +describe('src/cypress', () => { + describe('events', () => { + it('fail event', (done) => { + cy.on('fail', (err, runnable) => { + expect(err.message).to.equal('foo') + expect(runnable).to.equal(Cypress.state('runnable')) + + done() + }) + + throw new Error('foo') + }) + + it('viewport:changed event', () => { + let called = false + + cy.on('viewport:changed', (viewport) => { + expect(viewport).to.deep.equal({ viewportWidth: 100, viewportHeight: 100 }) + called = true + }) + + cy.viewport(100, 100).then(() => { + expect(called).to.be.true + }) + }) + + it('scrolled event', (done) => { + cy.viewport(100, 100) + Cypress.$('') + .attr('id', 'button') + .css({ + position: 'absolute', + left: '0px', + top: '50px', + }) + .appendTo(cy.$$('body')) + + cy.on('scrolled', ($el, type) => { + expect($el[0]).to.eq(Cypress.$('#button')[0]) + expect(type).to.eq('element') + + done() + }) + + cy.get('#button').trigger('mousedown') + }) + + context('command events', () => { + it('command:enqueued event', () => { + let called = false + + const handler = (command) => { + expect(command.name).to.eq('log') + called = true + cy.off('command:enqueued', handler) + } + + cy.on('command:enqueued', handler) + + cy.log('foo').then(() => { + expect(called).to.be.true + }) + }) + + it('command:start event', () => { + let called = false + + const handler = (command) => { + expect(command.attributes.name).to.eq('log') + called = true + cy.off('command:start', handler) + } + + cy.on('command:start', handler) + + cy.log('foo').then(() => { + expect(called).to.be.true + }) + }) + + it('command:end event', () => { + let called = false + + const handler = (command) => { + expect(command.attributes.name).to.eq('log') + called = true + cy.off('command:end', handler) + } + + cy.on('command:end', handler) + + cy.log('foo').then(() => { + expect(called).to.be.true + }) + }) + + it('command:retry event', (done) => { + const handler = (options) => { + expect(options._retries).to.equal(1) + expect(options.error.message).to.equal('Expected to find element: `#foo`, but never found it.') + done() + } + + cy.on('command:retry', handler) + + cy.get('#foo') + }) + }) + + context('log events', () => { + it('log:added event', () => { + const attrs: any[] = [] + const logs: any[] = [] + + const handler = (attr, log) => { + attrs.push(attr) + logs.push(log) + } + + cy.on('log:added', handler) + + Cypress.log({ name: 'log', message: `foo` }) + + cy.log('foo').then(() => { + expect(attrs[0].name).to.eq('log') + expect(logs[0].attributes.name).to.eq('log') + }) + }) + + it('log:changed event', (done) => { + const handler = (attr, log) => { + cy.off('log:changed', handler) + expect(attr.name).to.eq('bar') + expect(log.attributes.name).to.eq('bar') + done() + } + + const log = Cypress.log({ message: `foo` }) + + cy.on('log:changed', handler) + + log?.set('name', 'bar') + }) + }) + + // these tests need to be run together since they are testing lifecycle events + context('lifecycle (test:before/after:run) events', () => { + let afterRunnable + let beforeRunnable + let beforeAsyncRunnable + let expectedAfterRunnable + + const beforeHandler = (test, runnable) => { + expect(test.title).to.eq('test 2') + + beforeRunnable = runnable + Cypress.off('test:before:run', beforeHandler) + } + + const beforeAsyncHandler = (test, runnable) => { + expect(test.title).to.eq('test 2') + + beforeAsyncRunnable = runnable + Cypress.off('test:before:run:async', beforeAsyncHandler) + } + + const afterHandler = (test, runnable) => { + expect(test.title).to.eq('test 1') + + afterRunnable = runnable + Cypress.off('test:after:run', afterHandler) + } + + before(() => { + Cypress.on('test:before:run', beforeHandler) + Cypress.on('test:before:run:async', beforeAsyncHandler) + Cypress.on('test:after:run', afterHandler) + }) + + after(() => { + Cypress.off('test:before:run', beforeHandler) + Cypress.off('test:before:run:async', beforeAsyncHandler) + Cypress.off('test:after:run', afterHandler) + }) + + it('test 1', () => { + // this is the runnable that we expect to be passed to the test:after:run event + // and it will be verified in the next test since we need to wait for the test to finish + expectedAfterRunnable = Cypress.state('runnable') + }) + + it('test 2', () => { + // the before runnables should be from this test + const runnable = Cypress.state('runnable') + + // use === to avoid the circular references + expect(beforeAsyncRunnable === runnable).to.be.true + expect(beforeRunnable === runnable).to.be.true + + // the after runnable should be from the previous test + expect(afterRunnable).to.deep.equal(expectedAfterRunnable) + }) + }) + }) +}) diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index 19dd4f43e113..2d0ded980238 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -551,7 +551,7 @@ class $Cypress { // this event is how the reporter knows how to display // stats and runnable properties such as errors - this.emit('test:after:run', args[0], this.config('isInteractive')) + this.emit('test:after:run', ...args) this.maybeEmitCypressInCypress('mocha', 'test:after:run', args[0]) if (this.config('isTextTerminal')) {