diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index ce813922fd19..49da6eb71390 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -7,6 +7,10 @@ _Released 6/17/2025 (PENDING)_ - Install Cypress `win32-x64` binary on Windows `win32-arm64` systems. Cypress runs in emulation. Addresses [#30252](https://github.com/cypress-io/cypress/issues/30252). +**Bugfixes:** + +- Fixed an issue when using `Cypress.stop()` where a run may be aborted prior to receiving the required runner events causing Test Replay to not be available. Addresses [#31781](https://github.com/cypress-io/cypress/issues/31781). + ## 14.4.1 _Released 6/3/2025_ diff --git a/packages/driver/src/cypress/runner.ts b/packages/driver/src/cypress/runner.ts index d4c1b77ff9f9..8e14610ac4dc 100644 --- a/packages/driver/src/cypress/runner.ts +++ b/packages/driver/src/cypress/runner.ts @@ -556,6 +556,24 @@ const overrideRunnerHook = (Cypress, _runner, getTestById, getTest, setTest, get testAfterRun(test, Cypress) await testAfterRunAsync(test, Cypress) + + // if the user has stopped the run, we need to abort, + // this needs to happen after the test:after:run events have fired + // to ensure protocol can properly handle the abort + if (_runner.stopped) { + // abort the run + _runner.abort() + + // emit the final 'end' event + // since our reporter depends on this event + // and mocha may never fire this because our + // runnable may never finish + _runner.emit('end') + + // remove all the listeners + // so no more events fire + _runner.removeAllListeners() + } })] return newArgs @@ -1916,19 +1934,6 @@ export default { } _runner.stopped = true - - // abort the run - _runner.abort() - - // emit the final 'end' event - // since our reporter depends on this event - // and mocha may never fire this because our - // runnable may never finish - _runner.emit('end') - - // remove all the listeners - // so no more events fire - _runner.removeAllListeners() }, getDisplayPropsForLog: LogUtils.getDisplayProps, diff --git a/system-tests/__snapshots__/cypress-stop.spec.ts.js b/system-tests/__snapshots__/cypress-stop.spec.ts.js new file mode 100644 index 000000000000..b3de360c787f --- /dev/null +++ b/system-tests/__snapshots__/cypress-stop.spec.ts.js @@ -0,0 +1,271 @@ +exports['Cypress.stop() / stops execution when called in before'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (before.cy.js) │ + │ Searched: cypress/e2e/before.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: before.cy.js (1 of 1) + + + Cypress.stop() in before + ✓ should not run this test (NaNms) + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 1 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: before.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ before.cy.js XX:XX 2 1 - - 1 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 2 1 - - 1 + + +` + +exports['Cypress.stop() / stops execution when called in beforeEach'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (beforeEach.cy.js) │ + │ Searched: cypress/e2e/beforeEach.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: beforeEach.cy.js (1 of 1) + + + Cypress.stop() in beforeEach + ✓ should not run this test (NaNms) + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 1 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: beforeEach.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ beforeEach.cy.js XX:XX 2 1 - - 1 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 2 1 - - 1 + + +` + +exports['Cypress.stop() / stops execution when called in test'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (test.cy.js) │ + │ Searched: cypress/e2e/test.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: test.cy.js (1 of 1) + + + Cypress.stop() in test + ✓ should run this test + ✓ should stop during test execution + + 2 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 1 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: test.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ test.cy.js XX:XX 3 2 - - 1 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 3 2 - - 1 + + +` + +exports['Cypress.stop() / stops execution when called in afterEach'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (afterEach.cy.js) │ + │ Searched: cypress/e2e/afterEach.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: afterEach.cy.js (1 of 1) + + + Cypress.stop() in afterEach + ✓ should run this test + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 1 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: afterEach.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ afterEach.cy.js XX:XX 2 1 - - 1 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 2 1 - - 1 + + +` + +exports['Cypress.stop() / stops execution when called in after'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (after.cy.js) │ + │ Searched: cypress/e2e/after.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: after.cy.js (1 of 1) + + + Cypress.stop() in after + ✓ should run this test + + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: after.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ after.cy.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` diff --git a/system-tests/projects/cypress-stop/cypress.config.js b/system-tests/projects/cypress-stop/cypress.config.js new file mode 100644 index 000000000000..b0bbee46eb89 --- /dev/null +++ b/system-tests/projects/cypress-stop/cypress.config.js @@ -0,0 +1,5 @@ +module.exports = { + e2e: { + setupNodeEvents (on, config) {}, + }, +} diff --git a/system-tests/projects/cypress-stop/cypress/e2e/after.cy.js b/system-tests/projects/cypress-stop/cypress/e2e/after.cy.js new file mode 100644 index 000000000000..76acacb63a23 --- /dev/null +++ b/system-tests/projects/cypress-stop/cypress/e2e/after.cy.js @@ -0,0 +1,18 @@ +describe('Cypress.stop() in after', () => { + after(() => { + console.log('after 1') + }) + + after(() => { + Cypress.stop() + console.log('after 2') + }) + + after(() => { + console.log('after 3') + }) + + it('should run this test', () => { + cy.url().should('equal', 'about:blank') + }) +}) diff --git a/system-tests/projects/cypress-stop/cypress/e2e/afterEach.cy.js b/system-tests/projects/cypress-stop/cypress/e2e/afterEach.cy.js new file mode 100644 index 000000000000..39d763b8cdf1 --- /dev/null +++ b/system-tests/projects/cypress-stop/cypress/e2e/afterEach.cy.js @@ -0,0 +1,22 @@ +describe('Cypress.stop() in afterEach', () => { + afterEach(() => { + console.log('afterEach 1') + }) + + afterEach(() => { + Cypress.stop() + console.log('afterEach 2') + }) + + afterEach(() => { + console.log('afterEach 3') + }) + + it('should run this test', () => { + cy.url().should('equal', 'about:blank') + }) + + it('should not run this test', () => { + throw new Error('This test should not run') + }) +}) diff --git a/system-tests/projects/cypress-stop/cypress/e2e/before.cy.js b/system-tests/projects/cypress-stop/cypress/e2e/before.cy.js new file mode 100644 index 000000000000..c76714040330 --- /dev/null +++ b/system-tests/projects/cypress-stop/cypress/e2e/before.cy.js @@ -0,0 +1,22 @@ +describe('Cypress.stop() in before', () => { + before(() => { + console.log('before 1') + }) + + before(() => { + Cypress.stop() + console.log('before 2') + }) + + before(() => { + console.log('before 3') + }) + + it('should not run this test', () => { + throw new Error('This test should not run') + }) + + it('should also not run this test', () => { + throw new Error('This test should not run') + }) +}) diff --git a/system-tests/projects/cypress-stop/cypress/e2e/beforeEach.cy.js b/system-tests/projects/cypress-stop/cypress/e2e/beforeEach.cy.js new file mode 100644 index 000000000000..7de0784a106a --- /dev/null +++ b/system-tests/projects/cypress-stop/cypress/e2e/beforeEach.cy.js @@ -0,0 +1,22 @@ +describe('Cypress.stop() in beforeEach', () => { + beforeEach(() => { + console.log('beforeEach 1') + }) + + beforeEach(() => { + Cypress.stop() + console.log('beforeEach 2') + }) + + beforeEach(() => { + console.log('beforeEach 3') + }) + + it('should not run this test', () => { + throw new Error('This test should not run') + }) + + it('should also not run this test', () => { + throw new Error('This test should not run') + }) +}) diff --git a/system-tests/projects/cypress-stop/cypress/e2e/test.cy.js b/system-tests/projects/cypress-stop/cypress/e2e/test.cy.js new file mode 100644 index 000000000000..43d03066ddcc --- /dev/null +++ b/system-tests/projects/cypress-stop/cypress/e2e/test.cy.js @@ -0,0 +1,18 @@ +describe('Cypress.stop() in test', () => { + it('should run this test', () => { + console.log('test 1') + }) + + it('should stop during test execution', () => { + return Cypress.stop() + + // eslint-disable-next-line no-unreachable + console.log('test 2') + throw new Error('This code should not run') + }) + + it('should not run this test', () => { + console.log('test 3') + throw new Error('This test should not run') + }) +}) diff --git a/system-tests/projects/cypress-stop/cypress/support/e2e.js b/system-tests/projects/cypress-stop/cypress/support/e2e.js new file mode 100644 index 000000000000..59c84cbfd93f --- /dev/null +++ b/system-tests/projects/cypress-stop/cypress/support/e2e.js @@ -0,0 +1,38 @@ +before(() => { + console.log('global before') +}) + +beforeEach(() => { + console.log('global beforeEach') +}) + +afterEach(() => { + console.log('global afterEach') +}) + +after(() => { + console.log('global after') +}) + +Cypress.on('test:before:run:async', async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)) + console.log('test:before:run:async') +}) + +Cypress.on('test:before:run', () => { + console.log('test:before:run') +}) + +Cypress.on('test:before:after:run:async', async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)) + console.log('test:before:after:run:async') +}) + +Cypress.on('test:after:run', () => { + console.log('test:after:run') +}) + +Cypress.on('test:after:run:async', async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)) + console.log('test:after:run:async') +}) diff --git a/system-tests/test/cypress-stop.spec.ts b/system-tests/test/cypress-stop.spec.ts new file mode 100644 index 000000000000..1052e9b39f91 --- /dev/null +++ b/system-tests/test/cypress-stop.spec.ts @@ -0,0 +1,200 @@ +import systemTests from '../lib/system-tests' + +describe('Cypress.stop()', () => { + const getRunnableEventCounts = (stderr: string) => { + const beforeRunCount = (stderr.match(/test:before:run(?!:async)/g) || []).length + const beforeRunAsyncCount = (stderr.match(/test:before:run:async/g) || []).length + const beforeAfterRunAsyncCount = (stderr.match(/test:before:after:run:async/g) || []).length + const afterRunCount = (stderr.match(/test:after:run(?!:async)/g) || []).length + const afterRunAsyncCount = (stderr.match(/test:after:run:async/g) || []).length + + return { + beforeRunCount, + beforeRunAsyncCount, + beforeAfterRunAsyncCount, + afterRunCount, + afterRunAsyncCount, + } + } + + const getGlobalHooks = (stderr: string) => { + const globalBeforeCalled = (stderr.match(/global before(?!Each)/g) || []).length > 0 + const globalBeforeEachCalled = (stderr.match(/global beforeEach/g) || []).length > 0 + const globalAfterEachCalled = (stderr.match(/global afterEach/g) || []).length > 0 + const globalAfterCalled = (stderr.match(/global after(?!Each)/g) || []).length > 0 + + return { + globalBeforeCalled, + globalBeforeEachCalled, + globalAfterEachCalled, + globalAfterCalled, + } + } + + systemTests.setup() + + systemTests.it('stops execution when called in before', { + project: 'cypress-stop', + spec: 'before.cy.js', + snapshot: true, + expectedExitCode: 0, + browser: 'electron', + processEnv: { + ELECTRON_ENABLE_LOGGING: 1, + }, + onStderr: (stderr) => { + expect(stderr).to.include('before 1') + expect(stderr).to.include('before 2') + expect(stderr).to.not.include('before 3') + + const { globalBeforeCalled, globalBeforeEachCalled, globalAfterEachCalled, globalAfterCalled } = getGlobalHooks(stderr) + + expect(globalBeforeCalled, 'globalBeforeCalled').to.be.true + expect(globalBeforeEachCalled, 'globalBeforeEachCalled').to.be.false + expect(globalAfterEachCalled, 'globalAfterEachCalled').to.be.false + expect(globalAfterCalled, 'globalAfterCalled').to.be.false + + const { beforeRunCount, beforeRunAsyncCount, beforeAfterRunAsyncCount, afterRunCount, afterRunAsyncCount } = getRunnableEventCounts(stderr) + + expect(beforeRunCount, 'beforeRunCount').to.equal(1) + expect(beforeRunAsyncCount, 'beforeRunAsyncCount').to.equal(1) + expect(beforeAfterRunAsyncCount, 'beforeAfterRunAsyncCount').to.equal(1) + expect(afterRunCount, 'afterRunCount').to.equal(1) + expect(afterRunAsyncCount, 'afterRunAsyncCount').to.equal(1) + + return stderr + }, + }) + + systemTests.it('stops execution when called in beforeEach', { + project: 'cypress-stop', + spec: 'beforeEach.cy.js', + snapshot: true, + expectedExitCode: 0, + browser: 'electron', + processEnv: { + ELECTRON_ENABLE_LOGGING: 1, + }, + onStderr: (stderr) => { + expect(stderr).to.include('beforeEach 1') + expect(stderr).to.include('beforeEach 2') + expect(stderr).to.not.include('beforeEach 3') + + const { globalBeforeCalled, globalBeforeEachCalled, globalAfterEachCalled, globalAfterCalled } = getGlobalHooks(stderr) + + expect(globalBeforeCalled, 'globalBeforeCalled').to.be.true + expect(globalBeforeEachCalled, 'globalBeforeEachCalled').to.be.true + expect(globalAfterEachCalled, 'globalAfterEachCalled').to.be.false + expect(globalAfterCalled, 'globalAfterCalled').to.be.false + + const { beforeRunCount, beforeRunAsyncCount, beforeAfterRunAsyncCount, afterRunCount, afterRunAsyncCount } = getRunnableEventCounts(stderr) + + expect(beforeRunCount, 'beforeRunCount').to.equal(1) + expect(beforeRunAsyncCount, 'beforeRunAsyncCount').to.equal(1) + expect(beforeAfterRunAsyncCount, 'beforeAfterRunAsyncCount').to.equal(1) + expect(afterRunCount, 'afterRunCount').to.equal(1) + expect(afterRunAsyncCount, 'afterRunAsyncCount').to.equal(1) + + return stderr + }, + }) + + systemTests.it('stops execution when called in test', { + project: 'cypress-stop', + spec: 'test.cy.js', + snapshot: true, + expectedExitCode: 0, + browser: 'electron', + processEnv: { + ELECTRON_ENABLE_LOGGING: 1, + }, + onStderr: (stderr) => { + expect(stderr).to.include('test 1') + expect(stderr).to.not.include('test 2') + expect(stderr).to.not.include('test 3') + + const { globalBeforeCalled, globalBeforeEachCalled, globalAfterEachCalled, globalAfterCalled } = getGlobalHooks(stderr) + + expect(globalBeforeCalled, 'globalBeforeCalled').to.be.true + expect(globalBeforeEachCalled, 'globalBeforeEachCalled').to.be.true + expect(globalAfterEachCalled, 'globalAfterEachCalled').to.be.true + expect(globalAfterCalled, 'globalAfterCalled').to.be.false + + const { beforeRunCount, beforeRunAsyncCount, beforeAfterRunAsyncCount, afterRunCount, afterRunAsyncCount } = getRunnableEventCounts(stderr) + + expect(beforeRunCount, 'beforeRunCount').to.equal(2) + expect(beforeRunAsyncCount, 'beforeRunAsyncCount').to.equal(2) + expect(beforeAfterRunAsyncCount, 'beforeAfterRunAsyncCount').to.equal(2) + expect(afterRunCount, 'afterRunCount').to.equal(2) + expect(afterRunAsyncCount, 'afterRunAsyncCount').to.equal(2) + + return stderr + }, + }) + + systemTests.it('stops execution when called in afterEach', { + project: 'cypress-stop', + spec: 'afterEach.cy.js', + snapshot: true, + expectedExitCode: 0, + browser: 'electron', + processEnv: { + ELECTRON_ENABLE_LOGGING: 1, + }, + onStderr: (stderr) => { + expect(stderr).to.include('afterEach 1') + expect(stderr).to.include('afterEach 2') + expect(stderr).to.not.include('afterEach 3') + + const { globalBeforeCalled, globalBeforeEachCalled, globalAfterEachCalled, globalAfterCalled } = getGlobalHooks(stderr) + + expect(globalBeforeCalled, 'globalBeforeCalled').to.be.true + expect(globalBeforeEachCalled, 'globalBeforeEachCalled').to.be.true + expect(globalAfterEachCalled, 'globalAfterEachCalled').to.be.false + expect(globalAfterCalled, 'globalAfterCalled').to.be.false + + const { beforeRunCount, beforeRunAsyncCount, beforeAfterRunAsyncCount, afterRunCount, afterRunAsyncCount } = getRunnableEventCounts(stderr) + + expect(beforeRunCount, 'beforeRunCount').to.equal(1) + expect(beforeRunAsyncCount, 'beforeRunAsyncCount').to.equal(1) + expect(beforeAfterRunAsyncCount, 'beforeAfterRunAsyncCount').to.equal(1) + expect(afterRunCount, 'afterRunCount').to.equal(1) + expect(afterRunAsyncCount, 'afterRunAsyncCount').to.equal(1) + + return stderr + }, + }) + + systemTests.it('stops execution when called in after', { + project: 'cypress-stop', + spec: 'after.cy.js', + snapshot: true, + expectedExitCode: 0, + browser: 'electron', + processEnv: { + ELECTRON_ENABLE_LOGGING: 1, + }, + onStderr: (stderr) => { + expect(stderr).to.include('after 1') + expect(stderr).to.include('after 2') + expect(stderr).to.not.include('after 3') + + const { globalBeforeCalled, globalBeforeEachCalled, globalAfterEachCalled, globalAfterCalled } = getGlobalHooks(stderr) + + expect(globalBeforeCalled, 'globalBeforeCalled').to.be.true + expect(globalBeforeEachCalled, 'globalBeforeEachCalled').to.be.true + expect(globalAfterEachCalled, 'globalAfterEachCalled').to.be.true + expect(globalAfterCalled, 'globalAfterCalled').to.be.false + + const { beforeRunCount, beforeRunAsyncCount, beforeAfterRunAsyncCount, afterRunCount, afterRunAsyncCount } = getRunnableEventCounts(stderr) + + expect(beforeRunCount, 'beforeRunCount').to.equal(1) + expect(beforeRunAsyncCount, 'beforeRunAsyncCount').to.equal(1) + expect(beforeAfterRunAsyncCount, 'beforeAfterRunAsyncCount').to.equal(1) + expect(afterRunCount, 'afterRunCount').to.equal(1) + expect(afterRunAsyncCount, 'afterRunAsyncCount').to.equal(1) + + return stderr + }, + }) +})