diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 3a489cc16bfd..a8fa4d21a07b 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -670,7 +670,7 @@ declare namespace Cypress { * If validation fails after restoring a session, `setup` will re-run. * @default {false} */ - validate?: () => Promise | false | void + validate?: () => Promise | void } type CanReturnChainable = void | Chainable | Promise diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index 4e95616441d5..26670f450f24 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -28,7 +28,7 @@ const validateSetupSessionGroup = (isNewSession = true) => { describe('runner/cypress sessions.ui.spec', { // Limiting tests kept in memory due to large memory cost // of nested spec snapshots - numTestsKeptInMemory: 1, + numTestsKeptInMemory: 0, viewportWidth: 1000, viewportHeight: 1000, }, () => { @@ -127,7 +127,7 @@ describe('runner/cypress sessions.ui.spec', { .contains('runValidation') }) - cy.contains('CypressError') + cy.contains('AssertionError') // cy.percySnapshot() // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 }) @@ -276,7 +276,7 @@ describe('runner/cypress sessions.ui.spec', { // cy.percySnapshot() // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 - cy.get('.runnable-err').should('have.length', 1) + cy.get('.runnable-err') cy.get('.command-name-session').get('.command-expander').first().click() @@ -292,10 +292,8 @@ describe('runner/cypress sessions.ui.spec', { failCount: 1, }) - cy.get('.test').each(($el) => cy.wrap($el).click()) - cy.log('validate new session was created in first test') - cy.get('.test').eq(0).within(() => { + cy.get('.test').eq(0).click().within(() => { validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session').contains('created') @@ -355,34 +353,364 @@ describe('runner/cypress sessions.ui.spec', { }) describe('errors', () => { - it('test error when setup has failing Cypress command', () => { - loadSpec({ - projectName: 'session-and-origin-e2e-specs', - filePath: 'session/errors.cy.js', - failCount: 1, + describe('created session', () => { + before(() => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + failCount: 7, + }) }) - cy.contains('.test', 'setup has failing command').as('setup_failed') - // test marked as failed and is expanded - cy.get('@setup_failed').should('have.attr', 'data-model-state', 'failed') - .children('.collapsible').should('have.class', 'is-open') - .within(() => { - // session is marked as 'failed' and is expanded - // setup group is expanded - cy.get('.command-name-session').eq(0).should('contain', 'session_1').as('session_command') - .children('.command-wrapper').find('.reporter-tag').should('contain', 'failed') + it('setup has failing Cypress command', () => { + cy.contains('.test', 'setup - has failing command').as('example_test') + // test marked as failed and is expanded + cy.get('@example_test').should('have.attr', 'data-model-state', 'failed') + .children('.collapsible').should('have.class', 'is-open') + .within(() => { + // session is marked as 'failed' and is expanded + // setup group is expanded + cy.get('.command-name-session').eq(0).as('session_command') + .children('.command-wrapper').find('.reporter-tag').should('contain', 'failed') + + cy.get('@session_command') + .children('.command-child-container').should('exist') + .within(() => { + cy.contains('.command-wrapper', 'Create new session') + .should('have.class', 'command-state-failed') + .find('.failed-indicator') + .should('exist') + }) + }) + + const setupErrorPostFix = 'This error occurred while creating the session. Because the session setup failed, we failed the test.' + + cy.get('@example_test') + .find('.attempt-error-region') + .contains('Expected to find element') + .contains(setupErrorPostFix) + + cy.get('@example_test') + .find('.attempt-error-region') + .find('.test-err-code-frame') + .should('exist') + }) + + describe('failed validation', () => { + [ + { + testCase: 'has failing Cypress command', + systemTestTitle: 'validate - has failing Cypress command', + errMessage: 'failed because it requires a DOM element', + }, + { + testCase: 'command yields false', + systemTestTitle: 'validate - command yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'has multiple commands and yields false', + systemTestTitle: 'validate - has multiple commands and yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'rejects with false', + systemTestTitle: 'validate - rejects with false', + errMessage: 'rejected with false.', + }, + { + testCase: 'promise resolved false', + systemTestTitle: 'validate - promise resolves false', + errMessage: 'promise resolved false.', + }, + { + testCase: 'throws an error', + systemTestTitle: 'validate - throws an error', + errMessage: 'Something went wrong!', + }, + ].forEach((opts, index) => { + if (index !== 5) { + return + } + + const { testCase, systemTestTitle, errMessage } = opts + + it(`has test error when validate ${testCase}`, () => { + cy.contains('.test', systemTestTitle).as('example_test') + cy.get('@example_test') + .should('have.attr', 'data-model-state', 'failed') + .children('.collapsible') + .should('have.class', 'is-open') + .within(() => { + // session is marked as 'failed' and is expanded + // setup group is expanded + cy.get('.command-name-session').eq(0).as('session_command') + .children('.command-wrapper') + .find('.reporter-tag') + .should('contain', 'failed') + + cy.get('@session_command') + .children('.command-child-container') + .should('exist') + .within(() => { + // create session group is marked as 'passed' and is collapsed + cy.contains('.command-wrapper', 'Create new session') + .should('have.class', 'command-state-passed') + .children('.command-child-container') + .should('not.exist') + + cy.contains('.command-wrapper', 'Validate session').as('validateSessionGroup') + .should('have.class', 'command-state-failed') + .find('.failed-indicator') + .should('exist') + }) + }) + + const validateErrPostFix = 'This error occurred while validating the created session. Because validation failed immediately after creating the session, we failed the test.' + + cy.get('@example_test') + .find('.attempt-error-region') + .contains(errMessage) + .contains(validateErrPostFix) + + cy.get('@example_test') + .find('.attempt-error-region') + .find('.test-err-code-frame') + .should('exist') + }) + }) + }) + }) + + describe('recreated session', () => { + const assertRecreatedSession = (opts) => { + const { + testAlias, + validationErrMessage, + commandPassed, + successfullyRecreatedSession, + } = opts - cy.get('@session_command') - .children('.command-child-container').should('exist') + cy.get(testAlias) + .should('have.attr', 'data-model-state', commandPassed ? 'passed' : 'failed') + .children('.collapsible') + .should(commandPassed ? 'not.have.class' : 'have.class', 'is-open') + + if (commandPassed) { + cy.get(testAlias).scrollIntoView().click() + } + + cy.get(testAlias) .within(() => { - cy.get('.command-name-session') - .should('contain', 'Create new session') - .get('.command-child-container').should('exist') + // second session is marked as 'failed' and is expanded + cy.get('.command-name-session').eq(1).as('session_command') + .children('.command-wrapper') + .find('.reporter-tag') + .should('contain', commandPassed ? 'recreated' : 'failed') + + if (commandPassed) { + cy.get('@session_command') + .scrollIntoView() + .find('.command-expander') + .click() + } + + cy.get('@session_command') + .children('.command-child-container') + .should('exist') + .within(() => { + // restored session log + cy.contains('.command-wrapper', 'Restore saved session') + + cy.contains('.command-wrapper', 'Validate session').as('validateSessionGroup') + .should('have.class', 'command-state-failed') + .find('.failed-indicator') + .should('exist') + + const restoredMessagePostfix = 'This error occurred while validating the restored session. Because validation failed, we will try to recreate the session.' + + cy.get('@session_command') + .find('.recovered-test-err') + .contains(validationErrMessage) + .contains(restoredMessagePostfix) + + cy.get('@session_command') + .find('.recovered-test-err') + .find('.test-err-code-frame') + .should('exist') + + cy.contains('.command-wrapper', 'Recreate session') + .should('have.class', successfullyRecreatedSession ? 'command-state-passed' : 'command-state-failed') + .find('.failed-indicator') + .should(successfullyRecreatedSession ? 'not.exist' : 'exist', 'is-open') + }) + }) + } + + describe('successfully recreated session', () => { + before(() => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + passCount: 7, + failCount: 0, + setup () { + cy.window().then((win) => { + // @ts-ignore + return win.CYPRESS_TEST_DATA = { + restoreSessionWithValidationFailure: true, + successfullyRecreatedSession: true, + } + }) + }, + }) + }) + + ;[ + { + testCase: 'has failing Cypress command', + systemTestTitle: 'validate - has failing Cypress command', + errMessage: 'failed because it requires a DOM element', + }, + { + testCase: 'command yields false', + systemTestTitle: 'validate - command yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'has multiple commands and yields false', + systemTestTitle: 'validate - has multiple commands and yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'rejects with false', + systemTestTitle: 'validate - rejects with false', + errMessage: 'rejected with false.', + }, + { + testCase: 'promise resolved false', + systemTestTitle: 'validate - promise resolves false', + errMessage: 'promise resolved false.', + }, + { + testCase: 'throws an error', + systemTestTitle: 'validate - throws an error', + errMessage: 'Something went wrong!', + }, + ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { + it(`has test error when validate ${testCase}`, () => { + cy.contains('.test', systemTestTitle).as('example_test') + + cy.get('@example_test').within(() => { + assertRecreatedSession({ + testAlias: '@example_test', + validationErrMessage: errMessage, + commandPassed: true, + successfullyRecreatedSession: true, + }) + }) + + cy.get('@example_test') + .find('.attempt-error-region') + .should('not.exist') + }) }) }) - // has error - cy.get('@setup_failed').contains('This error occurred while creating session. Because the session setup failed, we failed the test.') + describe('failed to recreated session', () => { + before(() => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + passCount: 0, + failCount: 7, + setup () { + cy.window().then((win) => { + // @ts-ignore + return win.CYPRESS_TEST_DATA = { + restoreSessionWithValidationFailure: true, + successfullyRecreatedSession: false, + } + }) + }, + }) + }) + + it('setup has failing command', () => { + cy.contains('.test', 'setup - has failing command').as('example_test') + + cy.get('@example_test').within(() => { + assertRecreatedSession({ + testAlias: '@example_test', + validationErrMessage: 'callback yielded false', + commandPassed: false, + successfullyRecreatedSession: false, + }) + }) + + const recreatedErrPostfix = 'This error occurred while recreating the session. Because the session setup failed, we failed the test.' + + cy.get('@example_test') + .find('.attempt-error-region') + .contains('Expected to find element') + .contains(recreatedErrPostfix) + }) + + describe('failed validation', () => { + [ + { + testCase: 'has failing Cypress command', + systemTestTitle: 'validate - has failing Cypress command', + errMessage: 'failed because it requires a DOM element', + }, + { + testCase: 'command yields false', + systemTestTitle: 'validate - command yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'has multiple commands and yields false', + systemTestTitle: 'validate - has multiple commands and yields false', + errMessage: 'callback yielded false.', + }, + { + testCase: 'rejects with false', + systemTestTitle: 'validate - rejects with false', + errMessage: 'rejected with false.', + }, + { + testCase: 'promise resolved false', + systemTestTitle: 'validate - promise resolves false', + errMessage: 'promise resolved false.', + }, + { + testCase: 'throws an error', + systemTestTitle: 'validate - throws an error', + errMessage: 'Something went wrong!', + }, + ].forEach(({ testCase, systemTestTitle, errMessage }) => { + it(`has test error when validate ${testCase}`, () => { + cy.contains('.test', systemTestTitle).as('example_test') + + cy.get('@example_test').within(() => { + assertRecreatedSession({ + testAlias: '@example_test', + validationErrMessage: errMessage, + commandPassed: false, + successfullyRecreatedSession: true, + }) + }) + + const recreatedErrPostfix = 'This error occurred while validating the recreated session. Because validation failed immediately after recreating the session, we failed the test.' + + cy.get('@example_test') + .find('.attempt-error-region') + .contains(errMessage) + .contains(recreatedErrPostfix) + }) + }) + }) + }) }) }) }) diff --git a/packages/app/src/runner/logger.ts b/packages/app/src/runner/logger.ts index 82c04f943fcd..074d668369b9 100644 --- a/packages/app/src/runner/logger.ts +++ b/packages/app/src/runner/logger.ts @@ -8,9 +8,11 @@ interface Table { } interface Group { - items: any - label: boolean name: string + items?: any + label?: boolean + expand?: boolean + table?: boolean } export const logger = { @@ -93,7 +95,12 @@ export const logger = { const groups = this._getGroups(consoleProps) _.each(groups, (group) => { - console.groupCollapsed(group.name) + if (group.expand) { + console.group(group.name) + } else { + console.groupCollapsed(group.name) + } + _.each(group.items, (value, key) => { if (group.label === false) { this.log(value) @@ -102,6 +109,7 @@ export const logger = { } }) + this._logGroups(group) console.groupEnd() }) }, @@ -112,7 +120,7 @@ export const logger = { if (!groups) return return _.map(groups, (group) => { - group.items = this._formatted(group.items) + group.items = this._formatted(group.items || {}) return group }) diff --git a/packages/driver/cypress/e2e/commands/actions/click.cy.js b/packages/driver/cypress/e2e/commands/actions/click.cy.js index 3fa9e8bf358f..2dc1f0ec0e1b 100644 --- a/packages/driver/cypress/e2e/commands/actions/click.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/click.cy.js @@ -985,8 +985,8 @@ describe('src/cy/commands/actions/click', () => { cy.get('#ptrNone').click({ timeout: 300, force: true }) }) - it('should error with message about pointer-events', function () { - const onError = cy.stub().callsFake((err) => { + it('should error with message about pointer-events', function (done) { + cy.once('fail', (err) => { const { lastLog } = this expect(err.message).to.contain('has CSS `pointer-events: none`') @@ -1001,18 +1001,14 @@ describe('src/cy/commands/actions/click', () => { ]) expect(consoleProps['But it has CSS']).to.eq('pointer-events: none') + done() }) - cy.once('fail', onError) - cy.get('#ptrNone').click({ timeout: 300 }) - .then(() => { - expect(onError).calledOnce - }) }) - it('should error with message about pointer-events and include inheritance', function () { - const onError = cy.stub().callsFake((err) => { + it('should error with message about pointer-events and include inheritance', function (done) { + cy.once('fail', (err) => { const { lastLog } = this expect(err.message).to.contain('has CSS `pointer-events: none`, inherited from this element:') @@ -1030,14 +1026,10 @@ describe('src/cy/commands/actions/click', () => { expect(consoleProps['But it has CSS']).to.eq('pointer-events: none') expect(consoleProps['Inherited From']).to.eq(this.ptrNone.get(0)) + done() }) - cy.once('fail', onError) - cy.get('#ptrNoneChild').click({ timeout: 300 }) - .then(() => { - expect(onError).calledOnce - }) }) }) @@ -2096,10 +2088,8 @@ describe('src/cy/commands/actions/click', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - this.logs.push(log) + this.logs?.push(log) }) - - null }) it('throws when not a dom subject', (done) => { @@ -2239,7 +2229,7 @@ describe('src/cy/commands/actions/click', () => { const $btn = $('').attr('id', 'button-covered-in-span').prependTo(cy.$$('body')) const span = $('span on button').css({ position: 'absolute', left: $btn.offset().left, top: $btn.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).prependTo(cy.$$('body')) - cy.on('fail', (err) => { + cy.once('fail', (err) => { const { lastLog } = this // get + click logs @@ -4202,6 +4192,7 @@ describe('mouse state', () => { e.target.removeEventListener('pointerout', pointerout) }).as('pointerout') + const pointerleave = cy.stub().callsFake((e) => { const exp = { altKey: false, @@ -4239,6 +4230,7 @@ describe('mouse state', () => { e.target.removeEventListener('pointerleave', pointerleave) }).as('pointerleave') + const mouseover = cy.stub().callsFake((e) => { const exp = { altKey: false, @@ -4276,6 +4268,7 @@ describe('mouse state', () => { e.target.removeEventListener('mouseover', mouseover) }).as('mouseover') + const mouseenter = cy.stub().callsFake((e) => { const exp = { altKey: false, @@ -4313,6 +4306,7 @@ describe('mouse state', () => { e.target.removeEventListener('mouseenter', mouseenter) }).as('mouseenter') + const pointerover = cy.stub().callsFake((e) => { const exp = { altKey: false, @@ -4350,6 +4344,7 @@ describe('mouse state', () => { e.target.removeEventListener('pointerover', pointerover) }).as('pointerover') + const pointerenter = cy.stub().callsFake((e) => { const exp = { altKey: false, diff --git a/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js b/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js index 405857a55713..b93cea9658ea 100644 --- a/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/type_special_chars.cy.js @@ -1309,10 +1309,14 @@ describe('src/cy/commands/actions/type - #type special chars', () => { const state = log.get('state') if (state === 'pending') { - log.on('state:changed', (state) => { - return events.push(`${log.get('name')}:log:${state}`) - }) + events.push(`${log.get('name')}:log:${state}`) + } + }) + cy.on('command:log:changed', (attrs, log) => { + const state = log.get('state') + + if (state === 'pending') { events.push(`${log.get('name')}:log:${state}`) } }) @@ -1323,7 +1327,14 @@ describe('src/cy/commands/actions/type - #type special chars', () => { cy.get('#single-input input').type('f{enter}').then(() => { expect(events).to.deep.eq([ - 'get:start', 'get:log:pending', 'get:end', 'type:start', 'type:log:pending', 'submit', 'type:end', 'then:start', + 'get:start', + 'get:log:pending', + 'get:end', + 'type:start', + 'type:log:pending', + 'submit', + 'type:end', + 'then:start', ]) }) }) diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js index 04607fd0337e..b4048daab435 100644 --- a/packages/driver/cypress/e2e/commands/assertions.cy.js +++ b/packages/driver/cypress/e2e/commands/assertions.cy.js @@ -48,7 +48,7 @@ describe('src/cy/commands/assertions', () => { this.logs = [] cy.on('log:added', (attrs, log) => { - this.logs.push(log) + this.logs?.push(log) this.lastLog = log }) @@ -56,8 +56,8 @@ describe('src/cy/commands/assertions', () => { }) it('returns the subject for chainability', () => { - cy - .noop({ foo: 'bar' }).should('deep.eq', { foo: 'bar' }) + cy.noop({ foo: 'bar' }) + .should('deep.eq', { foo: 'bar' }) .then((obj) => { expect(testCommands()).to.eql([ { name: 'visit', snapshots: 1, retries: 0 }, @@ -368,7 +368,7 @@ describe('src/cy/commands/assertions', () => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'assert') { - logs.push(log) + logs?.push(log) if (logs.length === 3) { done() @@ -416,21 +416,26 @@ describe('src/cy/commands/assertions', () => { it('does not log extra commands on fail and properly fails command + assertions', function (done) { cy.on('fail', (err) => { assertLogLength(this.logs, 6) + expect(err.message).to.eq('You must provide a valid number to a `length` assertion. You passed: `asdf`') expect(this.logs[3].get('name')).to.eq('get') - expect(this.logs[3].get('state')).to.eq('failed') - expect(this.logs[3].get('error')).to.eq(err) + expect(this.logs[3].get('state')).to.eq('passed') + expect(this.logs[3].get('error')).to.be.undefined expect(this.logs[4].get('name')).to.eq('assert') expect(this.logs[4].get('state')).to.eq('failed') - expect(this.logs[4].get('error').name).to.eq('AssertionError') + expect(this.logs[4].get('error').name).to.eq('CypressError') + expect(this.logs[4].get('error')).to.eq(err) done() }) cy - .root().should('exist').and('contain', 'foo') - .get('button').should('have.length', 'asdf') + .root() + .should('exist') + .and('contain', 'foo') + .get('button') + .should('have.length', 'asdf') }) it('finishes failed assertions and does not log extra commands when cy.contains fails', function (done) { @@ -438,12 +443,13 @@ describe('src/cy/commands/assertions', () => { assertLogLength(this.logs, 2) expect(this.logs[0].get('name')).to.eq('contains') - expect(this.logs[0].get('state')).to.eq('failed') - expect(this.logs[0].get('error')).to.eq(err) + expect(this.logs[0].get('state')).to.eq('passed') + expect(this.logs[0].get('error')).to.be.undefined expect(this.logs[1].get('name')).to.eq('assert') expect(this.logs[1].get('state')).to.eq('failed') - expect(this.logs[1].get('error').name).to.eq('AssertionError') + expect(this.logs[1].get('error').name).to.eq('CypressError') + expect(this.logs[1].get('error')).to.eq(err) done() }) @@ -731,7 +737,7 @@ describe('src/cy/commands/assertions', () => { }) it('does not additionally log when .should is the current command', function (done) { - cy.on('fail', (err) => { + cy.once('fail', (err) => { const { lastLog } = this assertLogLength(this.logs, 1) @@ -822,7 +828,7 @@ describe('src/cy/commands/assertions', () => { this.logs = [] cy.on('log:added', (attrs, log) => { - this.logs.push(log) + this.logs?.push(log) if (attrs.name === 'assert') { this.lastLog = log @@ -1017,16 +1023,21 @@ describe('src/cy/commands/assertions', () => { cy.on('log:added', (attrs, log) => { if (attrs.name === 'assert') { cy.removeAllListeners('log:added') + let err - expect(log.invoke('consoleProps')).to.deep.eq({ - Command: 'assert', - expected: false, - actual: true, - Message: 'expected true to be false', - Error: log.get('error').stack, - }) + try { + expect(log.invoke('consoleProps')).to.deep.contain({ + Command: 'assert', + expected: false, + actual: true, + Message: 'expected true to be false', + Error: log.get('error').stack, + }) + } catch (e) { + err = e + } - done() + done(err) } }) @@ -1179,7 +1190,7 @@ describe('src/cy/commands/assertions', () => { this.logs = [] cy.on('log:added', (attrs, log) => { - this.logs.push(log) + this.logs?.push(log) }) return null @@ -1621,7 +1632,7 @@ describe('src/cy/commands/assertions', () => { } cy.on('log:added', (attrs, log) => { - this.logs.push(log) + this.logs?.push(log) }) return null @@ -2171,6 +2182,14 @@ describe('src/cy/commands/assertions', () => { }) it('visible, not visible, adds to error', function () { + cy.once('fail', (err) => { + const l6 = this.logs[5] + + // the error on this log should have this message appended to it + expect(l6.get('error').message).to.include(`expected '
' to be 'visible'`) + expect(err.message).to.include(`This element \`
\` is not visible because it has CSS property: \`display: none\``) + }) + expect(this.$div).to.be.visible // 1 expect(this.$div2).not.to.be.visible // 2 @@ -2187,15 +2206,7 @@ describe('src/cy/commands/assertions', () => { 'expected **
** not to be **visible**', ) - try { - expect(this.$div2).to.be.visible - } catch (err) { - const l6 = this.logs[5] - - // the error on this log should have this message appended to it - expect(l6.get('error').message).to.include(`expected '
' to be 'visible'`) - expect(l6.get('error').message).to.include(`This element \`
\` is not visible because it has CSS property: \`display: none\``) - } + expect(this.$div2).to.be.visible }) it('throws when obj is not DOM', function (done) { diff --git a/packages/driver/cypress/e2e/commands/connectors.cy.js b/packages/driver/cypress/e2e/commands/connectors.cy.js index 9d04419a777f..29f778df462c 100644 --- a/packages/driver/cypress/e2e/commands/connectors.cy.js +++ b/packages/driver/cypress/e2e/commands/connectors.cy.js @@ -1487,13 +1487,20 @@ describe('src/cy/commands/connectors', () => { }, () => { beforeEach(function () { this.logs = [] - - cy.on('log:added', (attrs, log) => { + const collectLogs = (attrs, log) => { if (attrs.name === 'its') { - this.lastLog = log + this.itsLog = log } + this.lastLog = log + this.logs?.push(log) + } + + cy.on('log:added', collectLogs) + + cy.on('fail', () => { + cy.off('log:added', collectLogs) }) return null @@ -1512,14 +1519,14 @@ describe('src/cy/commands/connectors', () => { it('throws when property does not exist', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `foo` does not exist on your subject.') expect(err.message).to.include('`cy.its()` waited for the specified property `foo` to exist, but it never did.') expect(err.message).to.include('If you do not expect the property `foo` to exist, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: \'bar\' }).its(\'quux\').should(\'not.exist\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1529,14 +1536,14 @@ describe('src/cy/commands/connectors', () => { it('throws when property is undefined', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `foo` returned a `undefined` value.') expect(err.message).to.include('`cy.its()` waited for the specified property `foo` to become accessible, but it never did.') expect(err.message).to.include('If you expect the property `foo` to be `undefined`, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: undefined }).its(\'foo\').should(\'be.undefined\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1546,14 +1553,14 @@ describe('src/cy/commands/connectors', () => { it('throws when property is null', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `foo` returned a `null` value.') expect(err.message).to.include('`cy.its()` waited for the specified property `foo` to become accessible, but it never did.') expect(err.message).to.include('If you expect the property `foo` to be `null`, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: null }).its(\'foo\').should(\'be.null\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1562,14 +1569,21 @@ describe('src/cy/commands/connectors', () => { }) it('throws the traversalErr as precedence when property does not exist even if the additional assertions fail', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + cy.once('fail', (err) => { + const { itsLog, lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `b` does not exist on your subject.') expect(err.message).to.include('`cy.its()` waited for the specified property `b` to exist, but it never did.') expect(err.message).to.include('If you do not expect the property `b` to exist, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: \'bar\' }).its(\'quux\').should(\'not.exist\')`') + expect(err.docsUrl).to.eq('https://on.cypress.io/its') + + expect(itsLog.get('state')).to.eq('passed') + expect(itsLog.get('error')).to.be.undefined + expect(lastLog.get('name')).to.eq('assert') + expect(lastLog.get('state')).to.eq('failed') + expect(lastLog.get('message')).to.contain('to be true') expect(lastLog.get('error').message).to.include(err.message) done() @@ -1579,14 +1593,21 @@ describe('src/cy/commands/connectors', () => { }) it('throws the traversalErr as precedence when property value is undefined even if the additional assertions fail', function (done) { - cy.on('fail', (err) => { - const { lastLog } = this + cy.once('fail', (err) => { + const { itsLog, lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `a` returned a `undefined` value.') expect(err.message).to.include('`cy.its()` waited for the specified property `a` to become accessible, but it never did.') expect(err.message).to.include('If you expect the property `a` to be `undefined`, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: undefined }).its(\'foo\').should(\'be.undefined\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') + + expect(itsLog.get('state')).to.eq('passed') + expect(itsLog.get('error')).to.be.undefined + + expect(lastLog.get('name')).to.eq('assert') + expect(lastLog.get('state')).to.eq('failed') + expect(lastLog.get('message')).to.contain('to be true') expect(lastLog.get('error').message).to.include(err.message) done() @@ -1606,11 +1627,18 @@ describe('src/cy/commands/connectors', () => { return 'baz' } - cy.on('fail', (err) => { - const { lastLog } = this + cy.once('fail', (err) => { + const { itsLog, lastLog } = this + expect(itsLog.invoke('consoleProps').Property).to.eq('.foo.bar.baz') + + expect(itsLog.get('state')).to.eq('passed') + expect(itsLog.get('error')).to.be.undefined + + expect(lastLog.get('name')).to.eq('assert') + expect(lastLog.get('state')).to.eq('failed') + expect(lastLog.get('message')).to.contain('expected **[Function]** to equal **baz**') expect(lastLog.get('error').message).to.include(err.message) - expect(lastLog.invoke('consoleProps').Property).to.eq('.foo.bar.baz') done() }) @@ -1640,12 +1668,11 @@ describe('src/cy/commands/connectors', () => { it('throws when reduced property does not exist on the subject', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `baz` does not exist on your subject.') expect(err.docsUrl).to.eq('https://on.cypress.io/its') - expect(lastLog.get('error').message).to.include(err.message) - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1690,11 +1717,11 @@ describe('src/cy/commands/connectors', () => { it('throws does not accept additional arguments', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('`cy.its()` does not accept additional arguments.') expect(err.docsUrl).to.eq('https://on.cypress.io/its') - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1714,10 +1741,10 @@ describe('src/cy/commands/connectors', () => { it('throws when options argument is not an object', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('`cy.its()` only accepts an object as the options argument.') - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1727,10 +1754,10 @@ describe('src/cy/commands/connectors', () => { it('throws when property name is missing', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('`cy.its()` expects the propertyName argument to have a value') - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1740,10 +1767,10 @@ describe('src/cy/commands/connectors', () => { it('throws when property name is not of type string', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog } = this expect(err.message).to.include('`cy.its()` only accepts a string or a number as the propertyName argument.') - expect(lastLog.get('error').message).to.include(err.message) + expect(itsLog.get('error').message).to.include(err.message) done() }) @@ -1757,9 +1784,16 @@ describe('src/cy/commands/connectors', () => { const obj = {} cy.on('fail', (err) => { - const { lastLog } = this + const { itsLog, lastLog } = this expect(err.message).to.include('Timed out retrying after 200ms: expected \'bar\' to equal \'baz\'') + + expect(itsLog.get('state')).to.eq('passed') + expect(itsLog.get('error')).to.be.undefined + + expect(lastLog.get('name')).to.eq('assert') + expect(lastLog.get('state')).to.eq('failed') + expect(lastLog.get('message')).to.contain('to equal') expect(lastLog.get('error').message).to.include(err.message) done() @@ -1776,10 +1810,10 @@ describe('src/cy/commands/connectors', () => { it('consoleProps subject', function (done) { cy.on('fail', (err) => { - expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ + expect(this.itsLog.invoke('consoleProps')).to.deep.eq({ Command: 'its', Property: '.fizz.buzz', - Error: this.lastLog.get('error').stack, + Error: this.itsLog.get('error').stack, Subject: { foo: 'bar' }, Yielded: undefined, }) diff --git a/packages/driver/cypress/e2e/commands/files.cy.js b/packages/driver/cypress/e2e/commands/files.cy.js index b5d2a8da64f7..8648ee4bae28 100644 --- a/packages/driver/cypress/e2e/commands/files.cy.js +++ b/packages/driver/cypress/e2e/commands/files.cy.js @@ -161,15 +161,23 @@ describe('src/cy/commands/files', () => { defaultCommandTimeout: 50, }, () => { beforeEach(function () { + const collectLogs = (attrs, log) => { + if (attrs.name === 'readFile') { + this.fileLog = log + } + + this.logs?.push(log) + } + cy.visit('/fixtures/empty.html') + .then(() => { + cy.on('log:added', collectLogs) + }) this.logs = [] - cy.on('log:added', (attrs, log) => { - if (attrs.name === 'readFile') { - this.lastLog = log - this.logs.push(log) - } + cy.on('fail', () => { + cy.off('log:added', collectLogs) }) return null @@ -177,11 +185,11 @@ describe('src/cy/commands/files', () => { it('throws when file argument is absent', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq('`cy.readFile()` must be passed a non-empty string as its 1st argument. You passed: `undefined`.') expect(err.docsUrl).to.eq('https://on.cypress.io/readfile') @@ -193,11 +201,11 @@ describe('src/cy/commands/files', () => { it('throws when file argument is not a string', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq('`cy.readFile()` must be passed a non-empty string as its 1st argument. You passed: `2`.') expect(err.docsUrl).to.eq('https://on.cypress.io/readfile') @@ -209,11 +217,11 @@ describe('src/cy/commands/files', () => { it('throws when file argument is an empty string', function (done) { cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq('`cy.readFile()` must be passed a non-empty string as its 1st argument. You passed: ``.') expect(err.docsUrl).to.eq('https://on.cypress.io/readfile') @@ -233,11 +241,11 @@ describe('src/cy/commands/files', () => { Cypress.backend.rejects(err) cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this - assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + assertLogLength(this.logs, 2) + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq(stripIndent`\ \`cy.readFile(\"foo\")\` failed while trying to read the file at the following path: @@ -265,11 +273,10 @@ describe('src/cy/commands/files', () => { Cypress.backend.rejects(err) cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this - assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq(stripIndent` Timed out retrying after 50ms: \`cy.readFile(\"foo.json\")\` failed because the file does not exist at the following path: @@ -300,11 +307,10 @@ describe('src/cy/commands/files', () => { }) cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this - assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq(stripIndent` Timed out retrying after 50ms: \`cy.readFile(\"foo.json\")\` failed because the file does not exist at the following path: @@ -325,11 +331,16 @@ describe('src/cy/commands/files', () => { Cypress.backend.resolves(okResponse) cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog, logs } = this - assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + const assertLog = logs.find((log) => log.get('name') === 'assert') + + expect(fileLog.get('state')).to.eq('passed') + expect(fileLog.get('error')).to.be.undefined + + expect(assertLog.get('name')).to.eq('assert') + expect(assertLog.get('error')).to.eq(err) + expect(assertLog.get('state')).to.eq('failed') expect(err.message).to.eq(stripIndent`\ Timed out retrying after 50ms: \`cy.readFile(\"foo.json\")\` failed because the file exists when expected not to exist at the following path: @@ -349,11 +360,15 @@ describe('src/cy/commands/files', () => { }) cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog, logs } = this - assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('state')).to.eq('passed') + expect(fileLog.get('error')).to.be.undefined + + const assertLog = logs.find((log) => log.get('name') === 'assert') + + expect(assertLog.get('error')).to.eq(err) + expect(assertLog.get('state')).to.eq('failed') expect(err.message).to.eq('Timed out retrying after 50ms: expected \'foo\' to equal \'contents\'') done() @@ -368,11 +383,10 @@ describe('src/cy/commands/files', () => { }) cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this - assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq(stripIndent`\ \`cy.readFile("foo")\` timed out after waiting \`10ms\`. `) @@ -393,11 +407,10 @@ describe('src/cy/commands/files', () => { }) cy.on('fail', (err) => { - const { lastLog } = this + const { fileLog } = this - assertLogLength(this.logs, 1) - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('state')).to.eq('failed') + expect(fileLog.get('error')).to.eq(err) + expect(fileLog.get('state')).to.eq('failed') expect(err.message).to.eq(stripIndent`\ \`cy.readFile("foo")\` timed out after waiting \`42ms\`. `) diff --git a/packages/driver/cypress/e2e/commands/navigation.cy.js b/packages/driver/cypress/e2e/commands/navigation.cy.js index 415686fba348..0d5e734d1ebd 100644 --- a/packages/driver/cypress/e2e/commands/navigation.cy.js +++ b/packages/driver/cypress/e2e/commands/navigation.cy.js @@ -115,7 +115,7 @@ describe('src/cy/commands/navigation', () => { cy.on('log:added', (attrs, log) => { this.lastLog = log - this.logs.push(log) + this.logs?.push(log) }) return null @@ -2104,7 +2104,7 @@ describe('src/cy/commands/navigation', () => { cy.on('log:added', (_attrs, log) => { this.lastLog = log - this.logs.push(log) + this.logs?.push(log) }) return null @@ -2124,17 +2124,15 @@ describe('src/cy/commands/navigation', () => { it('times out', function (done) { let thenCalled = false - cy.on('fail', (err, test) => { - if (test._currentRetry < 1) { - const { lastLog } = this - - // visit, window, page loading - assertLogLength(this.logs, 3) + cy.once('fail', (err, test) => { + const { lastLog } = this - expect(lastLog.get('name')).to.eq('page load') - expect(lastLog.get('error')).to.eq(err) - } + // visit, window, page loading + assertLogLength(this.logs, 3) + expect(lastLog.get('name')).to.eq('page load') + expect(lastLog.get('state')).to.eq('failed') + expect(lastLog.get('error')).to.eq(err) expect(err.message).to.include('Your page did not fire its `load` event within `50ms`.') return Promise @@ -2158,7 +2156,9 @@ describe('src/cy/commands/navigation', () => { causeSynchronousBeforeUnload($a) return null - }).wrap(null).then(() => { + }) + .wrap(null) + .then(() => { thenCalled = true }) }) diff --git a/packages/driver/cypress/e2e/commands/querying/querying.cy.js b/packages/driver/cypress/e2e/commands/querying/querying.cy.js index 412953cb491f..05266055a31e 100644 --- a/packages/driver/cypress/e2e/commands/querying/querying.cy.js +++ b/packages/driver/cypress/e2e/commands/querying/querying.cy.js @@ -346,7 +346,7 @@ describe('src/cy/commands/querying', () => { if (attrs.name === 'get') { this.lastLog = log - this.logs.push(log) + this.logs?.push(log) } }) @@ -738,12 +738,15 @@ describe('src/cy/commands/querying', () => { beforeEach(function () { this.logs = [] - cy.on('log:added', (attrs, log) => { - if (attrs.name === 'get') { - this.lastLog = log + const collectLogs = (attrs, log) => { + this.lastLog = log - this.logs.push(log) - } + this.logs?.push(log) + } + + cy.on('log:added', collectLogs) + cy.on('fail', () => { + cy.off('log:added', collectLogs) }) return null @@ -763,6 +766,7 @@ describe('src/cy/commands/querying', () => { const buttons = cy.$$('button') cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include(`Too many elements found. Found '${buttons.length}', expected '${buttons.length - 1}'.`) done() @@ -785,6 +789,7 @@ describe('src/cy/commands/querying', () => { it('throws after timing out not finding element', (done) => { cy.on('fail', (err) => { + expect(err.name).to.include('AssertionError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -795,6 +800,7 @@ describe('src/cy/commands/querying', () => { it('throws after timing out not finding element when should exist', (done) => { cy.on('fail', (err) => { + expect(err.name).to.include('AssertionError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -805,6 +811,7 @@ describe('src/cy/commands/querying', () => { it('throws existence error without running assertions', (done) => { cy.on('fail', (err) => { + expect(err.name).to.include('AssertionError') expect(err.message).to.include('Expected to find element: `#missing-el`, but never found it.') done() @@ -813,10 +820,20 @@ describe('src/cy/commands/querying', () => { cy.get('#missing-el').should('have.prop', 'foo') }) - it('throws when using an alias that does not exist') + it('throws when using an alias that does not exist', (done) => { + cy.on('fail', (err) => { + expect(err.name).to.include('CypressError') + expect(err.message).to.include('could not find a registered alias for: `@alias`.\nYou have not aliased anything yet.') + + done() + }) + + cy.get('@alias') + }) it('throws after timing out after a .wait() alias reference', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find element: `getJsonButton`, but never found it.') done() @@ -842,6 +859,7 @@ describe('src/cy/commands/querying', () => { it('throws after timing out while not trying to find an element', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected not to exist in the DOM, but it was continuously found.') done() @@ -852,6 +870,7 @@ describe('src/cy/commands/querying', () => { it('throws after timing out while trying to find an invisible element', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('expected \'\' not to be \'visible\'') done() @@ -860,8 +879,48 @@ describe('src/cy/commands/querying', () => { cy.get('div:first').should('not.be.visible') }) + it('fails get command when element is not found', function (done) { + cy.on('fail', (err) => { + const { lastLog } = this + + expect(err.name).to.eq('AssertionError') + expect(err.message).to.eq('Timed out retrying after 1ms: Expected to find element: `does_not_exist`, but never found it.') + + expect(lastLog.get('name')).to.eq('get') + expect(lastLog.get('state')).to.eq('failed') + expect(lastLog.get('error')).to.eq(err) + + done() + }) + + cy.get('does_not_exist', { timeout: 1 }) + }) + + it('fails get command when element is not found and has chained assertions', function (done) { + cy.once('fail', (err) => { + const { logs, lastLog } = this + const getLog = logs[logs.length - 2] + + expect(err.name).to.eq('AssertionError') + expect(err.message).to.eq('Timed out retrying after 1ms: Expected to find element: `does_not_exist`, but never found it.') + + expect(getLog.get('name')).to.eq('get') + expect(getLog.get('state')).to.eq('failed') + expect(getLog.get('error')).to.eq(err) + + expect(lastLog.get('name')).to.eq('assert') + expect(lastLog.get('state')).to.eq('failed') + expect(lastLog.get('error')).to.eq(err) + + done() + }) + + cy.get('does_not_exist', { timeout: 1 }).should('have.class', 'hi') + }) + it('does not include message about why element was not visible', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).not.to.include('why this element is not visible') done() @@ -962,16 +1021,24 @@ describe('src/cy/commands/querying', () => { const button = cy.$$('#button').hide() cy.on('fail', (err) => { - const { lastLog } = this + assertLogLength(this.logs, 2) - expect(lastLog.get('state')).to.eq('failed') - expect(lastLog.get('error')).to.eq(err) - expect(lastLog.get('$el').get(0)).to.eq(button.get(0)) - const consoleProps = lastLog.invoke('consoleProps') + const getLog = this.logs[0] + const assertionLog = this.logs[1] + + expect(err.message).to.contain('This element `` is not visible because it has CSS property: `display: none`') + + expect(getLog.get('state')).to.eq('passed') + expect(getLog.get('error')).to.be.undefined + expect(getLog.get('$el').get(0)).to.eq(button.get(0)) + const consoleProps = getLog.invoke('consoleProps') expect(consoleProps.Yielded).to.eq(button.get(0)) expect(consoleProps.Elements).to.eq(button.length) + expect(assertionLog.get('state')).to.eq('failed') + expect(err.message).to.include(assertionLog.get('error').message) + done() }) @@ -1560,7 +1627,7 @@ space this.lastLog = log } - this.logs.push(log) + this.logs?.push(log) }) return null @@ -1674,11 +1741,9 @@ space this.logs = [] cy.on('log:added', (attrs, log) => { - if (attrs.name === 'contains') { - this.lastLog = log + this.lastLog = log - this.logs.push(log) - } + this.logs?.push(log) }) return null @@ -1687,6 +1752,7 @@ space _.each([undefined, null], (val) => { it(`throws when text is ${val}`, (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('CypressError') expect(err.message).to.eq('`cy.contains()` can only accept a string, number or regular expression.') expect(err.docsUrl).to.eq('https://on.cypress.io/contains') @@ -1699,6 +1765,7 @@ space it('throws on a blank string', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('CypressError') expect(err.message).to.eq('`cy.contains()` cannot be passed an empty string.') expect(err.docsUrl).to.eq('https://on.cypress.io/contains') @@ -1728,6 +1795,7 @@ space it('throws when there is no filter and no subject', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find content: \'brand new content\' but never did.') done() @@ -1738,6 +1806,7 @@ space it('throws when there is a filter', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find content: \'brand new content\' within the selector: \'span\' but never did.') done() @@ -1748,6 +1817,7 @@ space it('throws when there is no filter but there is a subject', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find content: \'0\' within the element: but never did.') done() @@ -1758,6 +1828,7 @@ space it('throws when there is both a subject and a filter', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected to find content: \'foo\' within the element: and with the selector: \'ul\' but never did.') done() @@ -1768,6 +1839,7 @@ space it('throws after timing out while not trying to find an element that contains content', (done) => { cy.on('fail', (err) => { + expect(err.name).to.eq('AssertionError') expect(err.message).to.include('Expected not to find content: \'button\' but continuously found it.') done() @@ -1779,15 +1851,25 @@ space it('logs out $el when existing $el is found even on failure', function (done) { const button = cy.$$('#button') - cy.on('fail', (err) => { - expect(this.lastLog.get('state')).to.eq('failed') - expect(this.lastLog.get('error')).to.eq(err) - expect(this.lastLog.get('$el').get(0)).to.eq(button.get(0)) - const consoleProps = this.lastLog.invoke('consoleProps') + cy.once('fail', (err) => { + assertLogLength(this.logs, 2) + + const containsLog = this.logs[0] + const assertionLog = this.logs[1] + + expect(err.message).to.contain(`Expected not to find content: \'button\' but continuously found it.`) + + expect(containsLog.get('state')).to.eq('passed') + expect(containsLog.get('error')).to.be.undefined + expect(containsLog.get('$el').get(0)).to.eq(button.get(0)) + const consoleProps = containsLog.invoke('consoleProps') expect(consoleProps.Yielded).to.eq(button.get(0)) expect(consoleProps.Elements).to.eq(button.length) + expect(assertionLog.get('state')).to.eq('failed') + expect(err.message).to.include(assertionLog.get('error').message) + done() }) @@ -1795,11 +1877,21 @@ space }) it('throws when assertion is have.length > 1', function (done) { - cy.on('fail', (err) => { - assertLogLength(this.logs, 1) + cy.once('fail', (err) => { + assertLogLength(this.logs, 2) + + const containsLog = this.logs[0] + const assertionLog = this.logs[1] + expect(err.message).to.eq('`cy.contains()` cannot be passed a `length` option because it will only ever return 1 element.') expect(err.docsUrl).to.eq('https://on.cypress.io/contains') + expect(containsLog.get('state')).to.eq('passed') + expect(containsLog.get('error')).to.be.undefined + + expect(assertionLog.get('state')).to.eq('failed') + expect(err.message).to.include(assertionLog.get('error').message) + done() }) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index f7cdb6e4af74..5496293f081a 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -134,7 +134,7 @@ describe('cy.session', { retries: 0 }, () => { let validate const handleSetup = () => { - // create session clears page before running + // create session clears page before running cy.contains('Default blank page') cy.contains('This page was cleared by navigating to about:blank.') @@ -146,7 +146,7 @@ describe('cy.session', { retries: 0 }, () => { } const handleValidate = () => { - // both create & restore session clears page after running + // both create & restore session clears page after running cy.contains('Default blank page') cy.contains('This page was cleared by navigating to about:blank.') @@ -259,11 +259,18 @@ describe('cy.session', { retries: 0 }, () => { it('has session details in the consoleProps', () => { const consoleProps = logs[0].get('consoleProps')() - expect(consoleProps).to.deep.eq({ - Command: 'session', - id: 'session-1', - table: [], - }) + expect(consoleProps.Command).to.eq('session') + expect(consoleProps.id).to.eq('session-1') + expect(consoleProps.Domains).to.eq('This session captured data from localhost.') + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(1) + + const sessionStorageData = consoleProps.groups[0].groups[0] + + expect(sessionStorageData.name).to.contain('Session Storage - (1)') + expect(sessionStorageData.items).to.deep.eq({ cypressAuthToken: '{"body":{"username":"tester"}}' }) }) }) @@ -342,7 +349,7 @@ describe('cy.session', { retries: 0 }, () => { expect(setup).to.be.calledOnce expect(validate).to.be.calledOnce expect(clearPageCount, 'total times session cleared the page').to.eq(2) - expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') + expect(err.message).to.contain('This error occurred while validating the created session') expect(logs[0].get()).to.deep.contain({ name: 'session', id: sessionGroupId, @@ -388,7 +395,7 @@ describe('cy.session', { retries: 0 }, () => { done() }) - validate.callsFake(() => false) + validate.rejects(false) cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) }) @@ -525,11 +532,11 @@ describe('cy.session', { retries: 0 }, () => { sessionId = `session-${Cypress.state('test').id}` cy.session(sessionId, setup, { validate }) .then(() => { - // reset and only test restored session + // reset and only test restored session resetMocks() validate.callsFake(() => { if (validate.callCount === 1) { - return false + return Promise.reject(false) } handleValidate() @@ -588,15 +595,12 @@ describe('cy.session', { retries: 0 }, () => { group: validateSessionGroup.id, }) - expect(logs[6].get()).to.deep.contain({ - group: validateSessionGroup.id, - }) - - expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + // this error is associated with the group since the validation rejected + expect(logs[4].get('error').message).to.contain('This error occurred while validating the restored session') - validateClearLogs([logs[7], logs[8]], sessionGroupId) + validateClearLogs([logs[6], logs[7]], sessionGroupId) - const createNewSessionGroup = logs[9].get() + const createNewSessionGroup = logs[8].get() expect(createNewSessionGroup).to.contain({ displayName: 'Recreate session', @@ -604,24 +608,24 @@ describe('cy.session', { retries: 0 }, () => { group: sessionGroupId, }) - expect(logs[10].get()).to.deep.contain({ + expect(logs[9].get()).to.deep.contain({ alias: ['setupSession'], group: createNewSessionGroup.id, }) - expect(logs[11].get()).to.contain({ + expect(logs[10].get()).to.contain({ name: 'Clear page', group: createNewSessionGroup.id, }) - const secondValidateSessionGroup = logs[12].get() + const secondValidateSessionGroup = logs[11].get() expect(secondValidateSessionGroup).to.contain({ displayName: 'Validate session', group: sessionGroupId, }) - expect(logs[13].get()).to.deep.contain({ + expect(logs[12].get()).to.deep.contain({ alias: ['validateSession'], group: secondValidateSessionGroup.id, }) @@ -636,11 +640,11 @@ describe('cy.session', { retries: 0 }, () => { .then(() => { // reset and only test restored session resetMocks() - validate.callsFake(() => false) + validate.callsFake(() => Promise.reject(false)) }) cy.once('fail', (err) => { - expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') + expect(err.message).to.contain('Your `cy.session` **validate** promise rejected with false') expect(setup).to.be.calledOnce expect(validate).to.be.calledTwice expect(clearPageCount, 'total times session cleared the page').to.eq(3) @@ -681,15 +685,12 @@ describe('cy.session', { retries: 0 }, () => { group: validateSessionGroup.id, }) - expect(logs[6].get()).to.deep.contain({ - group: validateSessionGroup.id, - }) - - expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + // this error is associated with the group since the validation rejected + expect(logs[4].get('error').message).to.contain('Your `cy.session` **validate** promise rejected with false.') - validateClearLogs([logs[7], logs[8]], sessionGroupId) + validateClearLogs([logs[6], logs[7]], sessionGroupId) - const createNewSessionGroup = logs[9].get() + const createNewSessionGroup = logs[8].get() expect(createNewSessionGroup).to.contain({ displayName: 'Recreate session', @@ -697,24 +698,24 @@ describe('cy.session', { retries: 0 }, () => { group: sessionGroupId, }) - expect(logs[10].get()).to.deep.contain({ + expect(logs[9].get()).to.deep.contain({ alias: ['setupSession'], group: createNewSessionGroup.id, }) - expect(logs[11].get()).to.contain({ + expect(logs[10].get()).to.contain({ name: 'Clear page', group: createNewSessionGroup.id, }) - const secondValidateSessionGroup = logs[12].get() + const secondValidateSessionGroup = logs[11].get() expect(secondValidateSessionGroup).to.contain({ displayName: 'Validate session', group: sessionGroupId, }) - expect(logs[13].get()).to.deep.contain({ + expect(logs[12].get()).to.deep.contain({ alias: ['validateSession'], group: secondValidateSessionGroup.id, }) @@ -915,11 +916,18 @@ describe('cy.session', { retries: 0 }, () => { it('has session details in the consoleProps', () => { const consoleProps = logs[0].get('consoleProps')() - expect(consoleProps).to.deep.eq({ - Command: 'session', - id: 'session-1', - table: [], - }) + expect(consoleProps.Command).to.eq('session') + expect(consoleProps.id).to.eq('session-1') + expect(consoleProps.Domains).to.eq('This session captured data from localhost.') + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(1) + + const sessionStorageData = consoleProps.groups[0].groups[0] + + expect(sessionStorageData.name).to.contain('Session Storage - (1)') + expect(sessionStorageData.items).to.deep.eq({ cypressAuthToken: '{"body":{"username":"tester"}}' }) }) }) @@ -995,7 +1003,7 @@ describe('cy.session', { retries: 0 }, () => { expect(setup).to.be.calledOnce expect(validate).to.be.calledOnce expect(clearPageCount, 'total times session cleared the page').to.eq(0) - expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') + expect(err.message).to.contain('Your `cy.session` **validate** promise rejected with false') expect(logs[0].get()).to.deep.contain({ name: 'session', id: sessionGroupId, @@ -1039,7 +1047,7 @@ describe('cy.session', { retries: 0 }, () => { done() }) - validate.callsFake(() => false) + validate.callsFake(() => Promise.reject(false)) cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) }) @@ -1054,7 +1062,7 @@ describe('cy.session', { retries: 0 }, () => { sessionId = `session-${Cypress.state('test').id}` cy.session(sessionId, setup) .then(() => { - // reset and only test restored session + // reset and only test restored session resetMocks() }) @@ -1111,7 +1119,7 @@ describe('cy.session', { retries: 0 }, () => { sessionId = `session-${Cypress.state('test').id}` cy.session(sessionId, setup, { validate }) .then(() => { - // reset and only test restored session + // reset and only test restored session resetMocks() }) @@ -1184,7 +1192,7 @@ describe('cy.session', { retries: 0 }, () => { resetMocks() validate.callsFake(() => { if (validate.callCount === 1) { - return false + return Promise.reject(false) } handleValidate() @@ -1245,18 +1253,15 @@ describe('cy.session', { retries: 0 }, () => { group: validateSessionGroup.id, }) - expect(logs[5].get()).to.deep.contain({ - group: validateSessionGroup.id, - }) - - expect(logs[5].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + // this error is associated with the group since the validation rejected + expect(logs[3].get('error').message).to.contain('Your `cy.session` **validate** promise rejected with false.') - expect(logs[6].get()).to.contain({ + expect(logs[5].get()).to.contain({ displayName: 'Clear cookies, localStorage and sessionStorage', group: sessionGroupId, }) - const createNewSessionGroup = logs[7].get() + const createNewSessionGroup = logs[6].get() expect(createNewSessionGroup).to.contain({ displayName: 'Recreate session', @@ -1264,19 +1269,19 @@ describe('cy.session', { retries: 0 }, () => { group: sessionGroupId, }) - expect(logs[8].get()).to.deep.contain({ + expect(logs[7].get()).to.deep.contain({ alias: ['setupSession'], group: createNewSessionGroup.id, }) - const secondValidateSessionGroup = logs[9].get() + const secondValidateSessionGroup = logs[8].get() expect(secondValidateSessionGroup).to.contain({ displayName: 'Validate session', group: sessionGroupId, }) - expect(logs[10].get()).to.deep.contain({ + expect(logs[9].get()).to.deep.contain({ alias: ['validateSession'], group: secondValidateSessionGroup.id, }) @@ -1289,13 +1294,13 @@ describe('cy.session', { retries: 0 }, () => { cy.log('Creating new session for test') cy.session(`session-${Cypress.state('test').id}`, setup, { validate }) .then(() => { - // reset and only test restored session + // reset and only test restored session resetMocks() - validate.callsFake(() => false) + validate.callsFake(() => Promise.reject(false)) }) cy.once('fail', (err) => { - expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') + expect(err.message).to.contain('Your `cy.session` **validate** promise rejected with false') expect(setup).to.be.calledOnce expect(validate).to.be.calledTwice expect(clearPageCount, 'total times session cleared the page').to.eq(0) @@ -1339,18 +1344,15 @@ describe('cy.session', { retries: 0 }, () => { group: validateSessionGroup.id, }) - expect(logs[5].get()).to.deep.contain({ - group: validateSessionGroup.id, - }) - - expect(logs[5].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + // this error is associated with the group since the validation rejected + expect(logs[3].get('error').message).to.contain('Your `cy.session` **validate** promise rejected with false.') - expect(logs[6].get()).to.contain({ + expect(logs[5].get()).to.contain({ displayName: 'Clear cookies, localStorage and sessionStorage', group: sessionGroupId, }) - const createNewSessionGroup = logs[7].get() + const createNewSessionGroup = logs[6].get() expect(createNewSessionGroup).to.contain({ displayName: 'Recreate session', @@ -1358,19 +1360,19 @@ describe('cy.session', { retries: 0 }, () => { group: sessionGroupId, }) - expect(logs[8].get()).to.deep.contain({ + expect(logs[7].get()).to.deep.contain({ alias: ['setupSession'], group: createNewSessionGroup.id, }) - const secondValidateSessionGroup = logs[9].get() + const secondValidateSessionGroup = logs[8].get() expect(secondValidateSessionGroup).to.contain({ displayName: 'Validate session', group: sessionGroupId, }) - expect(logs[10].get()).to.deep.contain({ + expect(logs[9].get()).to.deep.contain({ alias: ['validateSession'], group: secondValidateSessionGroup.id, }) @@ -1492,6 +1494,7 @@ describe('cy.session', { retries: 0 }, () => { it('throws when options argument is provided and is not an object', function (done) { cy.once('fail', (err) => { + expect(lastSessionLog).to.eq(lastLog) expect(lastLog.get('error')).to.eq(err) expect(lastLog.get('state')).to.eq('failed') expect(err.message).to.eq('`cy.session()` was passed an invalid argument. The optional third argument `options` must be an object.') @@ -1598,7 +1601,7 @@ describe('cy.session', { retries: 0 }, () => { cy.once('fail', (err) => { expect(lastLog.get('error')).to.eq(err) expect(lastLog.get('state')).to.eq('failed') - expect(err.message).to.contain('This error occurred while creating session. Because the session setup failed, we failed the test.') + expect(err.message).to.contain('This error occurred while creating the session. Because the session setup failed, we failed the test.') expect(lastSessionLog.get('state')).to.eq('failed') done() }) @@ -1610,9 +1613,9 @@ describe('cy.session', { retries: 0 }, () => { it('throws when setup function has a failing assertion', function (done) { cy.once('fail', (err) => { - expect(lastLog.get('error')).to.eq(err) + expect(err.message).to.contain(lastLog.get('error').message) expect(lastLog.get('state')).to.eq('failed') - expect(err.message).to.contain('This error occurred while creating session. Because the session setup failed, we failed the test.') + expect(err.message).to.contain('This error occurred while creating the session. Because the session setup failed, we failed the test.') expect(lastSessionLog.get('state')).to.eq('failed') done() @@ -1625,7 +1628,7 @@ describe('cy.session', { retries: 0 }, () => { }) describe('options.validate failures', () => { - const errorHookMessage = 'This error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.' + const errorHookMessage = 'This error occurred while validating the created session. Because validation failed immediately after creating the session, we failed the test.' it('throws when options.validate has a failing Cypress command', (done) => { cy.once('fail', (err) => { @@ -1690,30 +1693,9 @@ describe('cy.session', { retries: 0 }, () => { }) }) - it('throws when options.validate returns false', (done) => { - cy.once('fail', (err) => { - expect(err.message).to.contain('Your `cy.session` **validate** callback returned false.') - expect(err.message).contain(errorHookMessage) - // TODO: Webkit does not have correct stack traces on errors currently - if (Cypress.isBrowser('!webkit')) { - expect(err.codeFrame).exist - } - - done() - }) - - cy.session(['mock-session', 'return false'], () => { - cy.log('setup') - }, { - validate () { - return false - }, - }) - }) - it('throws when options.validate resolves false', (done) => { cy.once('fail', (err) => { - expect(err.message).to.contain('Your `cy.session` **validate** callback resolved false.') + expect(err.message).to.contain('Your `cy.session` **validate** promise resolved false.') expect(err.message).contain(errorHookMessage) // TODO: Webkit does not have correct stack traces on errors currently if (Cypress.isBrowser('!webkit')) { @@ -1739,7 +1721,7 @@ describe('cy.session', { retries: 0 }, () => { it('throws when options.validate returns Chainer', (done) => { cy.once('fail', (err) => { - expect(err.message).to.contain('Your `cy.session` **validate** callback resolved false.') + expect(err.message).to.contain('Your `cy.session` **validate** callback yielded false.') expect(err.message).contain(errorHookMessage) done() }) diff --git a/packages/driver/cypress/e2e/commands/sessions/utils.cy.js b/packages/driver/cypress/e2e/commands/sessions/utils.cy.js index 00a79672c7cd..1276a10fd10e 100644 --- a/packages/driver/cypress/e2e/commands/sessions/utils.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/utils.cy.js @@ -4,6 +4,15 @@ const { } = require('@packages/driver/src/cy/commands/sessions/utils') describe('src/cy/commands/sessions/utils.ts', () => { + const logForDebugging = (consoleProps) => { + Cypress.log({ + name: 'debug', + message: 'click this log to view how this renders in the console', + event: true, + consoleProps, + }) + } + describe('.getConsoleProps', () => { it('for one domain with neither cookies or localStorage set', () => { const sessionState = { @@ -12,8 +21,10 @@ describe('src/cy/commands/sessions/utils.ts', () => { const consoleProps = getConsoleProps(sessionState) + logForDebugging(consoleProps) + expect(consoleProps.Warning).to.eq('⚠️ There are no cookies, local storage nor session storage associated with this session') expect(consoleProps.id).to.eq('session1') - expect(consoleProps.table).to.have.length(0) + expect(consoleProps.groups).to.have.length(0) }) it('for one domain with only cookies set', () => { @@ -26,12 +37,19 @@ describe('src/cy/commands/sessions/utils.ts', () => { const consoleProps = getConsoleProps(sessionState) + logForDebugging(consoleProps) + expect(consoleProps.id).to.eq('session1') - expect(consoleProps.table).to.have.length(1) - const cookiesTable = consoleProps.table[0]() + expect(consoleProps.Domains).to.eq('This session captured data from localhost.') + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(1) + + const cookieData = consoleProps.groups[0].groups[0] - expect(cookiesTable.name).to.contain('Cookies - localhost (1)') - expect(cookiesTable.data).to.deep.eq(sessionState.cookies) + expect(cookieData.name).to.contain('Cookies - (1)') + expect(cookieData.items).to.deep.eq(sessionState.cookies) }) it('for one domain with only localStorage set', () => { @@ -43,13 +61,43 @@ describe('src/cy/commands/sessions/utils.ts', () => { } const consoleProps = getConsoleProps(sessionState) + logForDebugging(consoleProps) + expect(consoleProps.id).to.eq('session1') - expect(consoleProps.table).to.have.length(1) - const localStorageTable = consoleProps.table[0]() + expect(consoleProps.Domains).to.eq('This session captured data from localhost.') + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(1) - expect(localStorageTable.name).to.contain('Storage - localhost (1)') - expect(localStorageTable.data).to.have.length(1) - expect(localStorageTable.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }]) + const localStorageData = consoleProps.groups[0].groups[0] + + expect(localStorageData.name).to.contain('Local Storage - (1)') + expect(localStorageData.items).to.deep.eq({ 'stor-foo': 's-f' }) + }) + + it('for one domain with only sessionStorage set', () => { + const sessionState = { + id: 'session1', + sessionStorage: [ + { origin: 'localhost', value: { 'stor-foo': 's-f' } }, + ], + } + const consoleProps = getConsoleProps(sessionState) + + logForDebugging(consoleProps) + + expect(consoleProps.id).to.eq('session1') + expect(consoleProps.Domains).to.eq('This session captured data from localhost.') + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(1) + + const sessionStorageData = consoleProps.groups[0].groups[0] + + expect(sessionStorageData.name).to.contain('Session Storage - (1)') + expect(sessionStorageData.items).to.deep.eq({ 'stor-foo': 's-f' }) }) it('for one domain with both cookies and localStorage set', () => { @@ -65,18 +113,23 @@ describe('src/cy/commands/sessions/utils.ts', () => { const consoleProps = getConsoleProps(sessionState) + logForDebugging(consoleProps) + expect(consoleProps.id).to.eq('session1') - expect(consoleProps.table).to.have.length(2) - let table = consoleProps.table[0]() + expect(consoleProps.Domains).to.eq('This session captured data from localhost.') + + expect(consoleProps.groups).to.have.length(1) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(2) + + const cookieData = consoleProps.groups[0].groups[0] + const localStorageData = consoleProps.groups[0].groups[1] - expect(table.name).to.contain('Cookies - localhost (1)') - expect(table.data).to.have.length(1) - expect(table.data).to.deep.eq(sessionState.cookies) + expect(cookieData.name).to.contain('Cookies - (1)') + expect(cookieData.items).to.deep.eq(sessionState.cookies) - table = consoleProps.table[1]() - expect(table.name).to.contain('Storage - localhost (1)') - expect(table.data).to.have.length(1) - expect(table.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }]) + expect(localStorageData.name).to.contain('Local Storage - (1)') + expect(localStorageData.items).to.deep.eq({ 'stor-foo': 's-f' }) }) it('for multiple domains', () => { @@ -94,23 +147,31 @@ describe('src/cy/commands/sessions/utils.ts', () => { const consoleProps = getConsoleProps(sessionState) + logForDebugging(consoleProps) + expect(consoleProps.id).to.eq('session1') - expect(consoleProps.table).to.have.length(3) - let table = consoleProps.table[0]() - - expect(table.name).to.contain('Cookies - localhost (2)') - expect(table.data).to.have.length(2) - expect(table.data).to.deep.eq(sessionState.cookies) - - table = consoleProps.table[1]() - expect(table.name).to.contain('Storage - localhost (1)') - expect(table.data).to.have.length(1) - expect(table.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }]) - - table = consoleProps.table[2]() - expect(table.name).to.contain('Storage - example.com (1)') - expect(table.data).to.have.length(1) - expect(table.data).to.deep.eq([{ key: 'random', value: 'hi' }]) + expect(consoleProps.Domains).to.eq('This session captured data from localhost and example.com.') + + expect(consoleProps.groups).to.have.length(2) + expect(consoleProps.groups[0].name).to.eq('localhost data:') + expect(consoleProps.groups[0].groups).to.have.length(2) + + const cookieData = consoleProps.groups[0].groups[0] + let localStorageData = consoleProps.groups[0].groups[1] + + expect(cookieData.name).to.contain('Cookies - (2)') + expect(cookieData.items).to.deep.eq(sessionState.cookies) + + expect(localStorageData.name).to.contain('Local Storage - (1)') + expect(localStorageData.items).to.deep.eq({ 'stor-foo': 's-f' }) + + expect(consoleProps.groups[1].name).to.eq('example.com data:') + expect(consoleProps.groups[1].groups).to.have.length(1) + + localStorageData = consoleProps.groups[1].groups[0] + + expect(localStorageData.name).to.contain('Local Storage - (1)') + expect(localStorageData.items).to.deep.eq({ 'random': 'hi' }) }) }) diff --git a/packages/driver/cypress/e2e/commands/traversals.cy.js b/packages/driver/cypress/e2e/commands/traversals.cy.js index 5f2f08b9402d..c884008b9d42 100644 --- a/packages/driver/cypress/e2e/commands/traversals.cy.js +++ b/packages/driver/cypress/e2e/commands/traversals.cy.js @@ -346,17 +346,28 @@ describe('src/cy/commands/traversals', () => { const button = cy.$$('#button').hide() cy.on('fail', (err) => { - const log = this.logs[1] + assertLogLength(this.logs, 3) - expect(log.get('state')).to.eq('failed') - expect(err.message).to.include(log.get('error').message) - expect(log.get('$el').get(0)).to.eq(button.get(0)) + const getLog = this.logs[0] + const findLog = this.logs[1] + const assertionLog = this.logs[2] - const consoleProps = log.invoke('consoleProps') + expect(err.message).to.contain('This element `` is not visible because it has CSS property: `display: none`') + + expect(getLog.get('state')).to.eq('passed') + expect(getLog.get('error')).to.be.undefined + + expect(findLog.get('state')).to.eq('passed') + expect(findLog.get('error')).to.be.undefined + expect(findLog.get('$el').get(0)).to.eq(button.get(0)) + const consoleProps = findLog.invoke('consoleProps') expect(consoleProps.Yielded).to.eq(button.get(0)) expect(consoleProps.Elements).to.eq(button.length) + expect(assertionLog.get('state')).to.eq('failed') + expect(err.message).to.include(assertionLog.get('error').message) + done() }) diff --git a/packages/driver/cypress/e2e/cypress/command_queue.cy.ts b/packages/driver/cypress/e2e/cypress/command_queue.cy.ts index be546a1bd627..91dc4871eeb3 100644 --- a/packages/driver/cypress/e2e/cypress/command_queue.cy.ts +++ b/packages/driver/cypress/e2e/cypress/command_queue.cy.ts @@ -27,14 +27,13 @@ describe('src/cypress/command_queue', () => { const state = (() => {}) as StateFunc const timeout = () => {} const whenStable = {} as IStability - const cleanup = () => 0 const fail = () => {} const isCy = () => true const clearTimeout = () => {} const setSubjectForChainer = () => {} beforeEach(() => { - queue = new CommandQueue(state, timeout, whenStable, cleanup, fail, isCy, clearTimeout, setSubjectForChainer) + queue = new CommandQueue(state, timeout, whenStable, fail, isCy, clearTimeout, setSubjectForChainer) queue.add(createCommand({ name: 'get', diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts index a73b2bed942b..5a19adac5668 100644 --- a/packages/driver/src/cy/assertions.ts +++ b/packages/driver/src/cy/assertions.ts @@ -81,7 +81,7 @@ const parseValueActualAndExpected = (value, actual, expected) => { export const create = (Cypress: ICypress, cy: $Cy) => { const getUpcomingAssertions = () => { - const index = cy.state('index') + 1 + const index = cy.queue.index + 1 const assertions: any[] = [] @@ -187,8 +187,6 @@ export const create = (Cypress: ICypress, cy: $Cy) => { _.extend(obj, { name: 'assert', - // end: true - // snapshot: true message, passed, selector: value ? value.selector : undefined, @@ -222,19 +220,26 @@ export const create = (Cypress: ICypress, cy: $Cy) => { return null } - const finishAssertions = () => { - cy.state('current').get('logs').forEach((log) => { - if (log.get('next') || !log.get('snapshots')) { - log.snapshot() - } + const finishAssertions = (err?: Error) => { + const logs = cy.state('current').get('logs') - const e = log.get('_error') + let hasLoggedError = false - if (e) { - return log.error(e) - } + logs.reverse().forEach((log, index) => { + if (log._shouldAutoEnd()) { + if (log.get('next') || !log.get('snapshots')) { + log.snapshot() + } + + // @ts-ignore + if (err && (!hasLoggedError || (err.issuesCommunicatingOrFinding && index === logs.length - 1))) { + hasLoggedError = true + + return log.error(err) + } - return log.end() + return log.end() + } }) } @@ -311,6 +316,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { cy.ensureCommandCanCommunicateWithAUT(err) ensureExistence() } catch (e2) { + e2.issuesCommunicatingOrFinding = true err = e2 } @@ -319,13 +325,10 @@ export const create = (Cypress: ICypress, cy: $Cy) => { options.error = err - if (err.retry === false) { - throw err - } - const { onFail, onRetry } = callbacks - if (!onFail && !onRetry) { + if (err.retry === false || (!onFail && !onRetry)) { + err.onFail = finishAssertions throw err } @@ -338,7 +341,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { onFail.call(this, err, isDefaultAssertionErr, cmds) } } catch (e3) { - finishAssertions() + e3.onFail = finishAssertions throw e3 } @@ -396,7 +399,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { const cmd = cmds[i] cmd.set('subject', subject) - cmd.skip() + cmd.skip() // technically this passed because it already ran }) return cmds @@ -442,15 +445,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => { // when we're told not to retry if (err.retry === false) { - // finish the assertions - finishAssertions() - - // and then push our command into this err - try { - $errUtils.throwErr(err, { onFail: options._log }) - } catch (e) { - err = e - } + throw $errUtils.throwErr(err, { onFail: finishAssertions }) } throw err diff --git a/packages/driver/src/cy/commands/actions/click.ts b/packages/driver/src/cy/commands/actions/click.ts index a14d43773ed9..b3f1d1692080 100644 --- a/packages/driver/src/cy/commands/actions/click.ts +++ b/packages/driver/src/cy/commands/actions/click.ts @@ -220,16 +220,16 @@ export default (Commands, Cypress, cy: $Cy, state, config) => { }, }) .catch((err) => { - // snapshot only on click failure - err.onFail = function () { - if (options._log) { - return options._log.snapshot() - } - } - // if we give up on waiting for actionability then // lets throw this error and log the command - return $errUtils.throwErr(err, { onFail: options._log }) + return $errUtils.throwErr(err, { + onFail (err) { + if (options._log) { + // snapshot only on click failure + options._log.snapshot().error(err) + } + }, + }) }) } diff --git a/packages/driver/src/cy/commands/agents.ts b/packages/driver/src/cy/commands/agents.ts index 611e2573a951..7bef1eebe782 100644 --- a/packages/driver/src/cy/commands/agents.ts +++ b/packages/driver/src/cy/commands/agents.ts @@ -68,7 +68,7 @@ const onInvoke = function (Cypress, obj, args) { const logProps: Record = { name: agentName, message: obj.message, - error: obj.error, + state: obj.error ? 'failed' : 'passed', type: 'parent', end: true, snapshot: !agent._noSnapshot, diff --git a/packages/driver/src/cy/commands/debugging.ts b/packages/driver/src/cy/commands/debugging.ts index 8ac152cf0604..f4e86c7217ba 100644 --- a/packages/driver/src/cy/commands/debugging.ts +++ b/packages/driver/src/cy/commands/debugging.ts @@ -19,18 +19,18 @@ const resume = (state, resumeAll = true) => { return onResume(resumeAll) } -const getNextQueuedCommand = (state, queue) => { +const getNextQueuedCommand = (queue) => { const search = (i) => { const cmd = queue.at(i) - if (cmd && cmd.get('skip')) { + if (cmd && cmd.state === 'skipped') { return search(i + 1) } return cmd } - return search(state('index')) + return search(queue.index) } interface InternalPauseOptions extends Partial { @@ -92,7 +92,7 @@ export default (Commands, Cypress, cy, state, config) => { } state('onPaused', (fn) => { - const next = getNextQueuedCommand(state, cy.queue) + const next = getNextQueuedCommand(cy.queue) // backup the current timeout const timeout = cy.timeout() diff --git a/packages/driver/src/cy/commands/misc.ts b/packages/driver/src/cy/commands/misc.ts index cc6a616979de..7a457f5fe4a3 100644 --- a/packages/driver/src/cy/commands/misc.ts +++ b/packages/driver/src/cy/commands/misc.ts @@ -29,7 +29,7 @@ export default (Commands, Cypress, cy, state) => { // when cy.log() is used inside it. // The code below restore the stack when cy.log() is injected in cy.then(). if (state('current').get('injected')) { - const restoreCmdIndex = state('index') + 1 + const restoreCmdIndex = cy.queue.index + 1 cy.queue.insert(restoreCmdIndex, $Command.create({ args: [cy.currentSubject()], @@ -37,7 +37,7 @@ export default (Commands, Cypress, cy, state) => { fn: (subject) => subject, })) - state('index', restoreCmdIndex) + cy.queue.index = restoreCmdIndex } Cypress.log({ diff --git a/packages/driver/src/cy/commands/querying/querying.ts b/packages/driver/src/cy/commands/querying/querying.ts index 64936aa91951..94990866aba2 100644 --- a/packages/driver/src/cy/commands/querying/querying.ts +++ b/packages/driver/src/cy/commands/querying/querying.ts @@ -497,7 +497,9 @@ export default (Commands, Cypress, cy, state) => { switch (err.type) { case 'length': if (err.expected > 1) { - return $errUtils.throwErrByPath('contains.length_option', { onFail: options._log }) + const assertionLog = Cypress.state('current').getLastLog() + + return $errUtils.throwErrByPath('contains.length_option', { onFail: assertionLog }) } break diff --git a/packages/driver/src/cy/commands/querying/within.ts b/packages/driver/src/cy/commands/querying/within.ts index 7b894e0f0066..8ac280a3dd96 100644 --- a/packages/driver/src/cy/commands/querying/within.ts +++ b/packages/driver/src/cy/commands/querying/within.ts @@ -23,7 +23,7 @@ export default (Commands, Cypress, cy, state) => { // https://github.com/cypress-io/cypress/pull/8699 // An internal command is inserted to create a divider between // commands inside within() callback and commands chained to it. - const restoreCmdIndex = state('index') + 1 + const restoreCmdIndex = cy.queue.index + 1 cy.queue.insert(restoreCmdIndex, $Command.create({ args: [subject], @@ -31,8 +31,6 @@ export default (Commands, Cypress, cy, state) => { fn: (subject) => subject, })) - state('index', restoreCmdIndex) - fn.call(cy.state('ctx'), subject) const cleanup = () => { diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index e29d98cff739..ef7b92ea34bf 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -1,13 +1,16 @@ import _ from 'lodash' import stringifyStable from 'json-stable-stringify' + import $errUtils from '../../../cypress/error_utils' -import $stackUtils from '../../../cypress/stack_utils' +import $utils from '../../../cypress/utils' import logGroup from '../../logGroup' import SessionsManager from './manager' import { getConsoleProps, navigateAboutBlank, + statusMap, } from './utils' + import type { ServerSessionData } from '@packages/types' type SessionData = Cypress.Commands.Session.SessionData @@ -20,10 +23,7 @@ type SessionData = Cypress.Commands.Session.SessionData * - if user relaunches the browser or launches a new spec, session data SHOULD be cleared * - session data SHOULD be cleared between specs in run mode */ - export default function (Commands, Cypress, cy) { - // @ts-ignore - function throwIfNoSessionSupport () { if (!Cypress.config('experimentalSessionAndOrigin')) { $errUtils.throwErrByPath('sessions.experimentNotEnabled', { @@ -39,6 +39,8 @@ export default function (Commands, Cypress, cy) { const sessionsManager = new SessionsManager(Cypress, cy) const sessions = sessionsManager.sessions + type SESSION_STEPS = 'create' | 'restore' | 'recreate' | 'validate' + Cypress.on('run:start', () => { // @ts-ignore Object.values(Cypress.state('activeSessions') || {}).forEach((sessionData: ServerSessionData) => { @@ -73,10 +75,17 @@ export default function (Commands, Cypress, cy) { $errUtils.throwErrByPath('sessions.session.wrongArgId') } + // stringify deterministically if we were given an object + id = _.isString(id) ? id : stringifyStable(id) + if (!setup || !_.isFunction(setup)) { $errUtils.throwErrByPath('sessions.session.wrongArgSetup') } + // backup session command so we can set it as codeFrame location for errors later on + const sessionCommand = cy.state('current') + const withinSubject = cy.state('withinSubject') + if (options) { if (!_.isObject(options)) { $errUtils.throwErrByPath('sessions.session.wrongArgOptions') @@ -102,12 +111,6 @@ export default function (Commands, Cypress, cy) { }) } - // backup session command so we can set it as codeFrame location for validation errors later on - const sessionCommand = cy.state('current') - - // stringify deterministically if we were given an object - id = _.isString(id) ? id : stringifyStable(id) - let session: SessionData = sessionsManager.getActiveSession(id) const isRegisteredSessionForSpec = sessionsManager.registeredSessions.has(id) @@ -157,29 +160,42 @@ export default function (Commands, Cypress, cy) { }) } - function createSession (existingSession: SessionData, recreateSession = false) { + function createSession (existingSession, step: 'create' | 'recreate') { logGroup(Cypress, { name: 'session', - displayName: recreateSession ? 'Recreate session' : 'Create new session', + displayName: statusMap.stepName(step), message: '', type: 'system', }, (setupLogGroup) => { return cy.then(async () => { // Catch when a cypress command fails in the setup function to correctly update log status // before failing command and ending command queue. - cy.state('onCommandFailed', (err) => { - setupLogGroup.set({ state: 'failed' }) + cy.state('onQueueFailed', (err, _queue) => { + if (!_.isObject(err)) { + err = new Error(err) + } + + setupLogGroup.set({ + state: 'failed', + consoleProps: () => { + return { + Step: statusMap.stepName(step), + Error: err?.stack || err?.message, + } + }, + }) + setSessionLogStatus('failed') - $errUtils.modifyErrMsg(err, `\n\nThis error occurred while creating session. Because the session setup failed, we failed the test.`, _.add) + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while ${statusMap.inProgress(step)} the session. Because the session setup failed, we failed the test.`, _.add) - return false + return err }) return existingSession.setup() }) .then(async () => { - cy.state('onCommandFailed', null) + cy.state('onQueueFailed', null) await navigateAboutBlank() const data = await sessions.getCurrentSessionData() @@ -187,28 +203,40 @@ export default function (Commands, Cypress, cy) { existingSession.hydrated = true _log.set({ consoleProps: () => getConsoleProps(existingSession) }) + setupLogGroup.set({ + consoleProps: () => { + return { + Step: statusMap.stepName(step), + ...getConsoleProps(existingSession), + } + }, + }) return }) }) } - function restoreSession (testSession) { - return cy.then(async () => { - Cypress.log({ - name: 'session', - displayName: 'Restore saved session', - message: '', - type: 'system', - }) + async function restoreSession (testSession) { + Cypress.log({ + name: 'session', + displayName: 'Restore saved session', + message: '', + type: 'system', + consoleProps: () => { + return { + Step: 'Restore saved session', + ...getConsoleProps(testSession), + } + }, + }) - _log.set({ consoleProps: () => getConsoleProps(testSession) }) + _log.set({ consoleProps: () => getConsoleProps(testSession) }) - await sessions.setSessionData(testSession) - }) + return sessions.setSessionData(testSession) } - function validateSession (existingSession, restoreSession = false) { + function validateSession (existingSession, step: SESSION_STEPS) { const isValidSession = true if (!existingSession.validate) { @@ -220,150 +248,211 @@ export default function (Commands, Cypress, cy) { displayName: 'Validate session', message: '', type: 'system', + consoleProps: () => { + return { + Step: 'Validate Session', + } + }, }, (validateLog) => { return cy.then(async () => { - const onSuccess = () => { - return isValidSession - } + const isValidSession = true + let caughtCommandErr = false + let _commandToRunAfterValidation + + const enhanceErr = (err) => { + Cypress.state('onQueueFailed', null) + if (typeof err !== 'object') { + err = new Error(err) + } - const onFail = (err) => { - validateLog.set({ state: 'failed' }) - setSessionLogStatus('failed') + err = $errUtils.enhanceStack({ + err, + userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), + projectRoot: Cypress.config('projectRoot'), + }) // show validation error and allow sessions workflow to recreate the session - if (restoreSession) { + if (step === 'restore') { + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the restored session. Because validation failed, we will try to recreate the session.`, _.add) + + // @ts-ignore err.isRecovered = true - Cypress.log({ - type: 'system', - name: 'session', + + validateLog.set({ + state: 'failed', + consoleProps: () => { + return { + Error: err.stack, + } + }, + // explicitly set via .set() so we don't end the log group early + ...(!caughtCommandErr && { error: err }), }) - .error(err) - return !isValidSession + return err } - $errUtils.modifyErrMsg(err, `\n\nThis error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.`, _.add) - - return cy.fail(err) + setSessionLogStatus('failed') + validateLog.set({ + state: 'failed', + consoleProps: () => { + return { + Error: err.stack, + } + }, + snapshot: true, + }) + + $errUtils.modifyErrMsg(err, `\n\nThis error occurred while validating the ${statusMap.complete(step)} session. Because validation failed immediately after ${statusMap.inProgress(step)} the session, we failed the test.`, _.add) + + return err } - return validate(existingSession, onSuccess, onFail) - }) - }) - } + cy.state('onQueueFailed', (err, queue): Error => { + if (typeof err !== 'object') { + err = new Error(err) + } - // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. - function validate (existingSession, onSuccess, onFail) { - let returnVal - let _validationError = null + if (step === 'restore') { + const commands = queue.get() + // determine command queue index of _commandToRunAfterValidation's index + let index = _.findIndex(commands, (command: any) => { + return ( + _commandToRunAfterValidation + && command.attributes.chainerId === _commandToRunAfterValidation.chainerId + ) + }) - try { - returnVal = existingSession.validate() - } catch (e) { - return onFail(e) - } + // skip all commands between this command which errored and _commandToRunAfterValidation + for (let i = cy.queue.index; i < index; i++) { + const cmd = commands[i] - // when the validate function returns a promise, ensure it does not return false or throw an error - if (typeof returnVal === 'object' && typeof returnVal.catch === 'function' && typeof returnVal.then === 'function') { - return returnVal - .then((val) => { - if (val === false) { - // set current command to cy.session for more accurate codeFrame - cy.state('current', sessionCommand) + if (!cmd.get('restore-within')) { + commands[i].skip() + } + } - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) - } + // restore within subject back to the original subject used when + // the session command kicked off + Cypress.state('withinSubject', withinSubject) - return onSuccess() - }) - .catch((err) => { - return onFail(err) - }) - } + // move to _commandToRunAfterValidation's index to ensure failures are + // handled correctly if next index was not found, the error was caused by + // a sync validation failure and _commandToRunAfterValidation is our next + // cmd + queue.index = index === -1 ? queue.index + 1 : index - // catch when a cypress command fails in the validate callback to move the queue index - cy.state('onCommandFailed', (err, queue) => { - const index = _.findIndex(queue.get(), (command: any) => { - return ( - _commandToRunAfterValidation - && command.attributes.chainerId === _commandToRunAfterValidation.chainerId - ) - }) + err.isRecovered = true - // attach codeframe and cleanse the stack trace since we will not hit the cy.fail callback - // if this is the first time validate fails - if (typeof err === 'string') { - err = new Error(err) - } + caughtCommandErr = true + } - err.stack = $stackUtils.normalizedStack(err) + return enhanceErr(err) + }) - _validationError = $errUtils.enhanceStack({ - err, - userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - projectRoot: Cypress.config('projectRoot'), - }) + let returnVal - // move to _commandToRunAfterValidation's index to ensure failures are handled correctly - cy.state('index', index) + try { + returnVal = existingSession.validate.call(cy.state('ctx')) + } catch (err) { + err.onFail = (err) => { + validateLog.set({ + error: err, + state: 'failed', + }) + } - cy.state('onCommandFailed', null) + throw err + } - return true - }) + _commandToRunAfterValidation = cy.then(async () => { + Cypress.state('onQueueFailed', null) - const _commandToRunAfterValidation = cy.then(async () => { - cy.state('onCommandFailed', null) + if (caughtCommandErr) { + return !isValidSession + } - if (_validationError) { - return onFail(_validationError) - } + const failValidation = (err) => { + if (step === 'restore') { + enhanceErr(err) - if (returnVal === false) { - // set current command to cy.session for more accurate codeframe - cy.state('current', sessionCommand) + // move to recreate session flow + return !isValidSession + } - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) - } + err.onFail = (err) => { + validateLog.error(err) + } - if (returnVal === undefined || Cypress.isCy(returnVal)) { - const val = cy.state('current').get('prev')?.attributes?.subject + throw enhanceErr(err) + } - if (val === false) { - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) - } - } + // when the validate function returns a promise, ensure it does not return false or throw an error + if ($utils.isPromiseLike(returnVal)) { + return returnVal + .then((val) => { + if (val === false) { + // set current command to cy.session for more accurate codeFrame + cy.state('current', sessionCommand) - return onSuccess() - }) + throw $errUtils.errByPath('sessions.validate_callback_false', { reason: 'promise resolved false' }) + } - return _commandToRunAfterValidation - } + return isValidSession + }) + .catch((err) => { + if (!(err instanceof Error)) { + // set current command to cy.session for more accurate codeFrame + cy.state('current', sessionCommand) + err = $errUtils.errByPath('sessions.validate_callback_false', { reason: `promise rejected with ${String(err)}` }) + } + + return failValidation(err) + }) + } + + if (returnVal === undefined || Cypress.isCy(returnVal)) { + const yielded = cy.state('current').get('prev')?.attributes?.subject + if (yielded === false) { + // set current command to cy.session for more accurate codeframe + cy.state('current', sessionCommand) + + return failValidation($errUtils.errByPath('sessions.validate_callback_false', { reason: 'callback yielded false' })) + } + } + + return isValidSession + }) + + return _commandToRunAfterValidation + }) + }) + } /** * Creates session flow: * 1. create session * 2. validate session */ - const createSessionWorkflow = (existingSession: SessionData, recreateSession = false) => { + const createSessionWorkflow = (existingSession, step: 'create' | 'recreate') => { return cy.then(async () => { - setSessionLogStatus(recreateSession ? 'recreating' : 'creating') + setSessionLogStatus(statusMap.inProgress(step)) await navigateAboutBlank() await sessions.clearCurrentSessionData() - return createSession(existingSession, recreateSession) + return createSession(existingSession, step) }) - .then(() => validateSession(existingSession)) + .then(() => validateSession(existingSession, step)) .then(async (isValidSession: boolean) => { if (!isValidSession) { - return + throw new Error('not a valid session :(') } sessionsManager.registeredSessions.set(existingSession.id, true) await sessions.saveSessionData(existingSession) - setSessionLogStatus(recreateSession ? 'recreated' : 'created') + setSessionLogStatus(statusMap.complete(step)) }) } @@ -381,10 +470,10 @@ export default function (Commands, Cypress, cy) { return restoreSession(existingSession) }) - .then(() => validateSession(existingSession, true)) + .then(() => validateSession(existingSession, 'restore')) .then((isValidSession: boolean) => { if (!isValidSession) { - return createSessionWorkflow(existingSession, true) + return createSessionWorkflow(existingSession, 'recreate') } setSessionLogStatus('restored') @@ -418,7 +507,7 @@ export default function (Commands, Cypress, cy) { _.extend(session, _.omit(serverStoredSession, 'setup', 'validate')) session.hydrated = true } else { - return createSessionWorkflow(session) + return createSessionWorkflow(session, 'create') } } diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts index 9078164e2805..91b814f3ec75 100644 --- a/packages/driver/src/cy/commands/sessions/manager.ts +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -10,15 +10,25 @@ import { type ActiveSessions = Cypress.Commands.Session.ActiveSessions type SessionData = Cypress.Commands.Session.SessionData -const getLogProperties = (displayName) => { +const LOGS = { + clearCurrentSessionData: { + displayName: 'Clear cookies, localStorage and sessionStorage', + consoleProps: { + Event: 'Cypress.session.clearCurrentSessionData()', + Details: 'Clearing the cookies, localStorage and sessionStorage across all domains. This ensures the session is created in clean browser context.', + }, + }, +} + +const getLogProperties = (apiName) => { return { name: 'sessions_manager', - displayName, message: '', - event: 'true', + event: true, state: 'passed', type: 'system', snapshot: false, + ...LOGS[apiName], } } @@ -139,7 +149,7 @@ export default class SessionsManager { clearCurrentSessionData: async () => { // this prevents a log occurring when we clear session in-between tests if (this.cy.state('duringUserTestExecution')) { - this.Cypress.log(getLogProperties('Clear cookies, localStorage and sessionStorage')) + this.Cypress.log(getLogProperties('clearCurrentSessionData')) } window.localStorage.clear() diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index 3932cd1c439d..0c4e7ba3c017 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -5,10 +5,11 @@ import { $Location } from '../../../cypress/location' type SessionData = Cypress.Commands.Session.SessionData -const getSessionDetailsForTable = (sessState: SessionData) => { +const getSessionDetailsByDomain = (sessState: SessionData) => { return _.merge( _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v })), ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: v } })), + ..._.map(sessState.sessionStorage, (v) => ({ [$Location.create(v.origin).hostname]: { sessionStorage: v } })), ) } @@ -97,39 +98,48 @@ const setPostMessageLocalStorage = async (specWindow, originOptions) => { }) } -const getConsoleProps = (sessState: SessionData) => { - const sessionDetails = getSessionDetailsForTable(sessState) - - const tables = _.flatMap(sessionDetails, (val, domain) => { - const cookiesTable = () => { - return { - name: `🍪 Cookies - ${domain} (${val.cookies.length})`, - data: val.cookies, - } - } - - const localStorageTable = () => { - return { - name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`, - data: _.map(val.localStorage.value, (value, key) => { - return { - key, - value, - } - }), - } +const getConsoleProps = (session: SessionData) => { + const sessionDetails = getSessionDetailsByDomain(session) + + const groupsByDomain = _.flatMap(sessionDetails, (val, domain) => { + return { + name: `${domain} data:`, + expand: true, + label: false, + groups: _.compact([ + val.cookies && { + name: `🍪 Cookies - (${val.cookies.length})`, + expand: true, + items: val.cookies, + }, + val.localStorage && { + name: `📁 Local Storage - (${_.keys(val.localStorage.value).length})`, + label: true, + expand: true, + items: val.localStorage.value, + }, + val.sessionStorage && { + name: `📁 Session Storage - (${_.keys(val.sessionStorage.value).length})`, + expand: true, + label: true, + items: val.sessionStorage.value, + }, + ]), } - - return [ - val.cookies && cookiesTable, - val.localStorage && localStorageTable, - ] }) - return { - id: sessState.id, - table: _.compact(tables), + const props = { + id: session.id, + ...(!groupsByDomain.length && { + Warning: '⚠️ There are no cookies, local storage nor session storage associated with this session', + }), + ...(groupsByDomain.length && { + Domains: `This session captured data from ${Object.keys(sessionDetails).join(' and ')}.`, + }), + groups: _.compact(groupsByDomain), } + + return props } const getPostMessageLocalStorage = (specWindow, origins): Promise => { @@ -193,10 +203,52 @@ function navigateAboutBlank (session: boolean = true) { return Cypress.action('cy:visit:blank', { type: session ? 'session' : 'session-lifecycle' }) as unknown as Promise } +const statusMap = { + inProgress: (step) => { + switch (step) { + case 'create': + return 'creating' + case 'restore': + return 'restoring' + case 'recreate': + return 'recreating' + default: + throw new Error(`${step} is not a valid session step.`) + } + }, + stepName: (step) => { + switch (step) { + case 'create': + return 'Create new session' + case 'restore': + return 'Restore saved session' + case 'recreate': + return 'Recreate session' + case 'validate': + return 'Validate session' + default: + throw new Error(`${step} is not a valid session step.`) + } + }, + complete: (step) => { + switch (step) { + case 'create': + return 'created' + case 'restore': + return 'restored' + case 'recreate': + return 'recreated' + default: + throw new Error(`${step} is not a valid session step.`) + } + }, +} + export { getCurrentOriginStorage, setPostMessageLocalStorage, getConsoleProps, getPostMessageLocalStorage, navigateAboutBlank, + statusMap, } diff --git a/packages/driver/src/cy/logGroup.ts b/packages/driver/src/cy/logGroup.ts index 0382e2c03178..307066dfcd20 100644 --- a/packages/driver/src/cy/logGroup.ts +++ b/packages/driver/src/cy/logGroup.ts @@ -22,7 +22,7 @@ export default (Cypress, userOptions: Cypress.LogGroup.Config, fn: Cypress.LogGr // An internal command is inserted to create a divider between // commands inside group() callback and commands chained to it. - const restoreCmdIndex = cy.state('index') + 1 + const restoreCmdIndex = cy.queue.index + 1 const endLogGroupCmd = $Command.create({ name: 'end-logGroup', diff --git a/packages/driver/src/cy/retries.ts b/packages/driver/src/cy/retries.ts index 088b0b2766c3..bc5e6207864a 100644 --- a/packages/driver/src/cy/retries.ts +++ b/packages/driver/src/cy/retries.ts @@ -25,7 +25,7 @@ type retryOptions = { } // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces -export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeout'], clearTimeout: $Cy['clearTimeout'], whenStable: $Cy['whenStable'], finishAssertions: (...args: any) => any) => ({ +export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeout'], clearTimeout: $Cy['clearTimeout'], whenStable: $Cy['whenStable'], finishAssertions: (err?: Error) => void) => ({ retry (fn, options: retryOptions, log?) { // remove the runnables timeout because we are now in retry // mode and should be handling timing out ourselves and dont @@ -70,8 +70,6 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou // if our total exceeds the timeout OR the total + the interval // exceed the runnables timeout, then bail if ((total + interval) >= options._runnableTimeout!) { - finishAssertions() - let onFail ({ error, onFail } = options) @@ -88,7 +86,13 @@ export const create = (Cypress: ICypress, state: StateFunc, timeout: $Cy['timeou const retryErr = mergeErrProps(error, retryErrProps) throwErr(retryErr, { - onFail: onFail || log, + onFail: (err) => { + if (onFail) { + err = onFail(err) + } + + finishAssertions(err) + }, }) } } diff --git a/packages/driver/src/cypress/command.ts b/packages/driver/src/cypress/command.ts index afcd6d0a391f..f0b7c14929bb 100644 --- a/packages/driver/src/cypress/command.ts +++ b/packages/driver/src/cypress/command.ts @@ -3,9 +3,11 @@ import utils from './utils' export class $Command { attributes!: Record + state: 'queued' | 'pending' | 'passed' | 'recovered' | 'failed' | 'skipped' constructor (attrs: any = {}) { this.reset() + this.state = 'queued' // if the command came from a secondary origin, it already has an id if (!attrs.id) { @@ -18,6 +20,26 @@ export class $Command { this.set(attrs) } + pass () { + this.state = 'passed' + } + + skip () { + this.state = 'skipped' + } + + fail () { + this.state = 'failed' + } + + recovered () { + this.state = 'recovered' + } + + start () { + this.state = 'pending' + } + set (key, val?) { let obj @@ -102,10 +124,6 @@ export class $Command { } } - skip () { - return this.set('skip', true) - } - stringify () { let { name, args } = this.attributes diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 26a23d8ee02f..b8b42012b803 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -19,40 +19,46 @@ const __stackReplacementMarker = (fn, args) => { return fn(...args) } -const commandRunningFailed = (Cypress, state, err) => { +const commandRunningFailed = (Cypress, err, current?: $Command) => { // allow for our own custom onFail function - if (err.onFail) { + if (err.onFail && _.isFunction(err.onFail)) { err.onFail(err) - // clean up this onFail callback after it's been called delete err.onFail return } - const current = state('current') + const lastLog = current?.getLastLog() - return Cypress.log({ - end: true, - snapshot: true, - error: err, - consoleProps () { - if (!current) return + const consoleProps = () => { + if (!current) return + + const consoleProps = {} + const prev = current.get('prev') + + if (current.get('type') === 'parent' || !prev) return - const consoleProps = {} - const prev = current.get('prev') + // if type isn't parent then we know its dual or child + // and we can add Applied To if there is a prev command + // and it is a parent + consoleProps['Applied To'] = $dom.isElement(prev.get('subject')) ? + $dom.getElements(prev.get('subject')) : + prev.get('subject') - if (current.get('type') === 'parent' || !prev) return + return consoleProps + } - // if type isn't parent then we know its dual or child - // and we can add Applied To if there is a prev command - // and it is a parent - consoleProps['Applied To'] = $dom.isElement(prev.get('subject')) ? - $dom.getElements(prev.get('subject')) : - prev.get('subject') + // ensure the last log on the command ends correctly + if (lastLog && !lastLog.get('ended')) { + return lastLog.set({ consoleProps }).error(err) + } - return consoleProps - }, + return Cypress.log({ + end: true, + snapshot: true, + error: err, + consoleProps, }) } @@ -60,7 +66,6 @@ export class CommandQueue extends Queue<$Command> { state: StateFunc timeout: $Cy['timeout'] stability: IStability - cleanup: $Cy['cleanup'] fail: $Cy['fail'] isCy: $Cy['isCy'] clearTimeout: ITimeouts['clearTimeout'] @@ -70,21 +75,22 @@ export class CommandQueue extends Queue<$Command> { state: StateFunc, timeout: $Cy['timeout'], stability: IStability, - cleanup: $Cy['cleanup'], fail: $Cy['fail'], isCy: $Cy['isCy'], clearTimeout: ITimeouts['clearTimeout'], setSubjectForChainer: $Cy['setSubjectForChainer'], ) { super() + this.state = state this.timeout = timeout this.stability = stability - this.cleanup = cleanup this.fail = fail this.isCy = isCy this.clearTimeout = clearTimeout this.setSubjectForChainer = setSubjectForChainer + + this.run = this.run.bind(this) } logs (filter) { @@ -105,6 +111,36 @@ export class CommandQueue extends Queue<$Command> { return _.invokeMap(this.get(), 'get', 'name') } + enqueue (command: $Command) { + // if we have a nestedIndex it means we're processing + // nested commands and need to insert them into the + // index past the current index as opposed to + // pushing them to the end we also dont want to + // reset the run defer because splicing means we're + // already in a run loop and dont want to create another! + // we also reset the .next property to properly reference + // our new obj + + // we had a bug that would bomb on custom commands when it was the + // first command. this was due to nestedIndex being undefined at that + // time. so we have to ensure to check that its any kind of number (even 0) + // in order to know to insert it into the existing array. + let nestedIndex = this.state('nestedIndex') + + // if this is a number, then we know we're about to insert this + // into our commands and need to reset next + increment the index + if (_.isNumber(nestedIndex) && nestedIndex < this.length) { + this.state('nestedIndex', (nestedIndex += 1)) + } + + // we look at whether or not nestedIndex is a number, because if it + // is then we need to insert inside of our commands, else just push + // it onto the end of the queue + const index = _.isNumber(nestedIndex) ? nestedIndex : this.length + + this.insert(index, command) + } + insert (index: number, command: $Command) { super.insert(index, command) @@ -132,12 +168,25 @@ export class CommandQueue extends Queue<$Command> { }) } - /** - * Check if the current command index is the last command in the queue - * @returns boolean - */ - isOnLastCommand (): boolean { - return this.state('index') === this.length + cleanup () { + const runnable = this.state('runnable') + + if (runnable && !runnable.isPending()) { + // make sure we reset the runnable's timeout now + runnable.resetTimeout() + } + + // if a command fails then after each commands + // could also fail unless we clear this out + this.state('commandIntermediateValue', undefined) + + // reset the nestedIndex back to null + this.state('nestedIndex', null) + + // and forcibly move the index needle to the + // end in case we have after / afterEach hooks + // which need to run + this.index = this.length } private runCommand (command: $Command) { @@ -146,14 +195,14 @@ export class CommandQueue extends Queue<$Command> { // prior to ever making it through our first // command if (this.stopped) { - return + return Promise.resolve() } this.state('current', command) this.state('chainerId', command.get('chainerId')) return this.stability.whenStable(() => { - this.state('nestedIndex', this.state('index')) + this.state('nestedIndex', this.index) return command.get('args') }) @@ -180,6 +229,7 @@ export class CommandQueue extends Queue<$Command> { // run the command's fn with runnable's context try { + command.start() ret = __stackReplacementMarker(command.get('fn'), args) } catch (err) { throw err @@ -227,9 +277,8 @@ export class CommandQueue extends Queue<$Command> { } return ret - }).then((subject) => { - this.state('commandIntermediateValue', undefined) - + }) + .then((subject) => { // we may be given a regular array here so // we need to re-wrap the array in jquery // if that's the case if the first item @@ -250,18 +299,21 @@ export class CommandQueue extends Queue<$Command> { } command.set({ subject }) + command.pass() // end / snapshot our logs if they need it command.finishLogs() - // reset the nestedIndex back to null - this.state('nestedIndex', null) - - // we're finished with the current command so set it back to null - this.state('current', null) - this.setSubjectForChainer(command.get('chainerId'), subject) + this.state({ + commandIntermediateValue: undefined, + // reset the nestedIndex back to null + nestedIndex: null, + // we're finished with the current command so set it back to null + current: null, + }) + return subject }) } @@ -269,32 +321,32 @@ export class CommandQueue extends Queue<$Command> { // TypeScript doesn't allow overriding functions with different type signatures // @ts-ignore run () { - const next = () => { - // bail if we've been told to abort in case - // an old command continues to run after - if (this.stopped) { - return - } + if (this.stopped) { + this.cleanup() - // start at 0 index if one is not already set - let index = this.state('index') || this.state('index', 0) + return Promise.resolve() + } - const command = this.at(index) + const next = () => { + const command = this.at(this.index) - // if the command should be skipped, just bail and increment index - if (command && command.get('skip')) { + // if the command has already ran or should be skipped, just bail and increment index + if (command && (command.state === 'passed' || command.state === 'skipped')) { // must set prev + next since other // operations depend on this state being correct command.set({ - prev: this.at(index - 1), - next: this.at(index + 1), + prev: this.at(this.index - 1), + next: this.at(this.index + 1), }) - this.state('index', index + 1) - this.setSubjectForChainer(command.get('chainerId'), command.get('subject')) - Cypress.action('cy:skipped:command:end', command) + if (command.state === 'skipped') { + Cypress.action('cy:skipped:command:end', command) + } + + // move on to the next queueable + this.index += 1 return next() } @@ -310,6 +362,13 @@ export class CommandQueue extends Queue<$Command> { // move onto the next test until its finished return this.stability.whenStable(() => { Cypress.action('cy:command:queue:end') + this.stop() + + const onQueueEnd = cy.state('onQueueEnd') + + if (onQueueEnd) { + onQueueEnd() + } return null }) @@ -337,27 +396,21 @@ export class CommandQueue extends Queue<$Command> { // and we reset the timeout again, it will always // cause a timeout later no matter what. by this time // mocha expects the test to be done - let fn if (!runnable.state) { this.timeout(prevTimeout) } - // mutate index by incrementing it - // this allows us to keep the proper index - // in between different hooks like before + beforeEach - // else run will be called again and index would start - // over at 0 - index += 1 - this.state('index', index) - Cypress.action('cy:command:end', command) - fn = this.state('onPaused') + // move on to the next queueable + this.index += 1 - if (fn) { + const pauseFn = this.state('onPaused') + + if (pauseFn) { return new Bluebird((resolve) => { - return fn(resolve) + return pauseFn(resolve) }).then(next) } @@ -365,26 +418,24 @@ export class CommandQueue extends Queue<$Command> { }) } - const onError = (err: Error | string) => { + const onError = (err) => { // If the runnable was marked as pending, this test was skipped // go ahead and just return const runnable = this.state('runnable') if (runnable.isPending()) { + this.stop() + return } - if (this.state('onCommandFailed')) { - const handledError = this.state('onCommandFailed')(err, this) + if (this.state('onQueueFailed')) { + err = this.state('onQueueFailed')(err, this) - cy.state('onCommandFailed', null) - - if (handledError) { - return next() - } + this.state('onQueueFailed', null) } - debugErrors('caught error in promise chain: %o', err) + debugErrors('error throw while executing cypress queue: %o', err) // since this failed this means that a specific command failed // and we should highlight it in red or insert a new command @@ -394,7 +445,23 @@ export class CommandQueue extends Queue<$Command> { err.name = 'CypressError' } - commandRunningFailed(Cypress, this.state, err) + const current = this.state('current') + + commandRunningFailed(Cypress, err, current) + + if (err.isRecovered) { + current?.recovered() + + return // let the queue end & restart on to the next command index (set in onQueueFailed) + } + + if (current?.state === 'queued') { + current.skip() + } else if (current?.state === 'pending') { + current.fail() + } + + this.cleanup() return this.fail(err) } @@ -402,7 +469,7 @@ export class CommandQueue extends Queue<$Command> { const { promise, reject, cancel } = super.run({ onRun: next, onError, - onFinish: this.cleanup, + onFinish: this.run, }) this.state('promise', promise) diff --git a/packages/driver/src/cypress/commands.ts b/packages/driver/src/cypress/commands.ts index 13ee2648c62a..979d5ec11dde 100644 --- a/packages/driver/src/cypress/commands.ts +++ b/packages/driver/src/cypress/commands.ts @@ -84,8 +84,6 @@ export default { } const Commands = { - _commands: commands, // for testing - each (fn) { // perf loop for (let name in commands) { diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 4419cb07d0d3..b46e04403c8e 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -249,7 +249,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.onBeforeAppWindowLoad = this.onBeforeAppWindowLoad.bind(this) this.onUncaughtException = this.onUncaughtException.bind(this) this.setRunnable = this.setRunnable.bind(this) - this.cleanup = this.cleanup.bind(this) this.setSubjectForChainer = this.setSubjectForChainer.bind(this) // init traits @@ -362,7 +361,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.overrides = createOverrides(state, config, focused, snapshots) - this.queue = new CommandQueue(state, this.timeout, stability, this.cleanup, this.fail, this.isCy, this.clearTimeout, this.setSubjectForChainer) + this.queue = new CommandQueue(state, this.timeout, stability, this.fail, this.isCy, this.clearTimeout, this.setSubjectForChainer) setTopOnError(Cypress, this) @@ -471,6 +470,8 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert // 1. callback with state("done") when async // 2. throw the error for the promise chain try { + this.Cypress.state('logGroupIds', []) // reset log groups so assertions are at the top level + // collect all of the callbacks for 'fail' rets = this.Cypress.action('cy:fail', err, this.state('runnable')) } catch (cyFailErr: any) { @@ -691,19 +692,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert runQueue () { cy.queue.run() - .then(() => { - const onQueueEnd = cy.state('onQueueEnd') - - if (onQueueEnd) { - onQueueEnd() - } - }) - .catch(() => { - // errors from the queue are propagated to cy.fail by the queue itself - // and can be safely ignored here. omitting this catch causes - // unhandled rejections to be logged because Bluebird sees a promise - // chain with no catch handler - }) } addCommand ({ name, fn, type, prevSubject }) { @@ -1096,27 +1084,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert return this.Cypress.action('app:navigation:changed', `page navigation event (${event})`) } - private cleanup () { - // cleanup could be called during a 'stop' event which - // could happen in between a runnable because they are async - if (this.state('runnable')) { - // make sure we reset the runnable's timeout now - this.state('runnable').resetTimeout() - } - - // if a command fails then after each commands - // could also fail unless we clear this out - this.state('commandIntermediateValue', undefined) - - // reset the nestedIndex back to null - this.state('nestedIndex', null) - - // and forcibly move the index needle to the - // end in case we have after / afterEach hooks - // which need to run - return this.state('index', this.queue.length) - } - private contentWindowListeners (contentWindow) { const cy = this @@ -1183,33 +1150,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert } private enqueue (obj: PartialBy) { - // if we have a nestedIndex it means we're processing - // nested commands and need to insert them into the - // index past the current index as opposed to - // pushing them to the end we also dont want to - // reset the run defer because splicing means we're - // already in a run loop and dont want to create another! - // we also reset the .next property to properly reference - // our new obj - - // we had a bug that would bomb on custom commands when it was the - // first command. this was due to nestedIndex being undefined at that - // time. so we have to ensure to check that its any kind of number (even 0) - // in order to know to insert it into the existing array. - let nestedIndex = this.state('nestedIndex') - - // if this is a number, then we know we're about to insert this - // into our commands and need to reset next + increment the index - if (_.isNumber(nestedIndex) && nestedIndex < this.queue.length) { - this.state('nestedIndex', (nestedIndex += 1)) - } - - // we look at whether or not nestedIndex is a number, because if it - // is then we need to insert inside of our commands, else just push - // it onto the end of the queue - const index = _.isNumber(nestedIndex) ? nestedIndex : this.queue.length - - this.queue.insert(index, $Command.create(obj)) + this.queue.enqueue($Command.create(obj)) return this.Cypress.action('cy:command:enqueued', obj) } @@ -1364,7 +1305,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert } private doneEarly () { - this.queue.stop() + this.queue.cleanup() // we only need to worry about doneEarly when // it comes from a manual event such as stopping @@ -1379,7 +1320,5 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert this.state('canceled', true) this.state('cancel')() } - - return this.cleanup() } } diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 19b8b1b2ccfb..9b3d9f1864a6 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1728,7 +1728,7 @@ export default { sessions: { validate_callback_false: { - message: 'Your `cy.session` **validate** callback {{reason}}.', + message: 'Your `cy.session` **validate** {{reason}}.', }, experimentNotEnabled ({ experimentalSessionSupport }) { if (experimentalSessionSupport) { diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index d40e497f8669..36f65e99571d 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -212,12 +212,12 @@ const makeErrFromErr = (err, options: any = {}) => { // assume onFail is a command if // onFail is present and isn't a function if (onFail && !_.isFunction(onFail)) { - const command = onFail + const log = onFail // redefine onFail and automatically // hook this into our command onFail = (err) => { - return command.error(err) + return log.error(err) } } @@ -262,7 +262,8 @@ const warnByPath = (errPath, options: any = {}) => { } export class InternalCypressError extends Error { - onFail?: undefined | Function + onFail?: Function + isRecovered?: boolean constructor (message) { super(message) @@ -279,7 +280,8 @@ export class CypressError extends Error { docsUrl?: string retry?: boolean userInvocationStack?: any - onFail?: undefined | Function + onFail?: Function + isRecovered?: boolean constructor (message) { super(message) diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 4a66aab3b287..75b131db2cc4 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -3,7 +3,6 @@ import $ from 'jquery' import clone from 'clone' import { HIGHLIGHT_ATTR } from '../cy/snapshots' -import { extend as extendEvents } from './events' import $dom from '../dom' import $utils from './utils' import $errUtils from './error_utils' @@ -245,8 +244,6 @@ export class Log { // only fire the log:state:changed event as fast as every 4ms this.fireChangeEvent = _.debounce(fireChangeEvent, 4) this.obj = defaults(state, config, obj) - - extendEvents(this) } get (attr) { @@ -401,7 +398,7 @@ export class Log { error (err) { const logGroupIds = this.state('logGroupIds') || [] - // current log was responsible to creating the current log group so end the current group + // current log was responsible for creating the current log group so end the current group if (_.last(logGroupIds) === this.attributes.id) { this.endGroup() } @@ -409,6 +406,7 @@ export class Log { this.set({ ended: true, error: err, + _error: undefined, state: 'failed', }) @@ -544,7 +542,7 @@ export class Log { } // add note if no snapshot exists on command instruments - if ((_this.get('instrument') === 'command') && !_this.get('snapshots')) { + if ((_this.get('instrument') === 'command') && _this.get('snapshot') && !_this.get('snapshots')) { consoleObj.Snapshot = 'The snapshot is missing. Displaying current state of the DOM.' } else { delete consoleObj.Snapshot @@ -556,7 +554,7 @@ export class Log { } class LogManager { - logs: Record = {} + logs: Record = {} constructor () { this.fireChangeEvent = this.fireChangeEvent.bind(this) @@ -580,8 +578,6 @@ class LogManager { if (!_.isEqual(log._emittedAttrs, attrs)) { log._emittedAttrs = attrs - log.emit(event, attrs) - return Cypress.action(event, attrs, log) } } @@ -641,7 +637,6 @@ class LogManager { log.wrapConsoleProps() this.addToLogs(log) - if (options.emitOnly) { return } diff --git a/packages/driver/src/cypress/runner.ts b/packages/driver/src/cypress/runner.ts index cb545a8f3b1b..b618adeeb57b 100644 --- a/packages/driver/src/cypress/runner.ts +++ b/packages/driver/src/cypress/runner.ts @@ -1492,7 +1492,7 @@ export default { // attach error right now // if we have one if (err) { - const PendingErrorMessages = ['sync skip', 'async skip call', 'async skip; aborting execution'] + const PendingErrorMessages = ['sync skip', 'sync skip; aborting execution', 'async skip call', 'async skip; aborting execution'] if (_.find(PendingErrorMessages, err.message) !== undefined) { err.isPending = true diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index af8a25d9df90..1b838f9b6404 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -482,16 +482,19 @@ const normalizedUserInvocationStack = (userInvocationStack) => { // add/$Chainer.prototype[key] (cypress:///../driver/src/cypress/chainer.js:30:128) // whereas Chromium browsers have the user's line first const stackLines = getStackLines(userInvocationStack) - const winnowedStackLines = _.reject(stackLines, (line) => { - // WARNING: STACK TRACE WILL BE DIFFERENT IN DEVELOPMENT vs PRODUCTOIN + const nonCypressStackLines = _.reject(stackLines, (line) => { + // WARNING: STACK TRACE WILL BE DIFFERENT IN DEVELOPMENT vs PRODUCTION // stacks in development builds look like: // at cypressErr (cypress:///../driver/src/cypress/error_utils.js:259:17) // stacks in prod builds look like: // at cypressErr (http://localhost:3500/isolated-runner/cypress_runner.js:173123:17) - return line.includes('cy[name]') || line.includes('Chainer.prototype[key]') || line.includes('cy.') || line.includes('$Chainer.') + return line.includes('cy[name]') + || line.includes('Chainer.prototype[key]') + || line.includes('cy.') + || line.includes('$Chainer.') }).join('\n') - return normalizeStackIndentation(winnowedStackLines) + return normalizeStackIndentation(nonCypressStackLines) } export default { diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index d417a72d39c2..1b6e3914b4ce 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -31,7 +31,6 @@ export interface StateFunc { (k: 'runnable', v?: CypressRunnable): CypressRunnable (k: 'isStable', v?: boolean): boolean (k: 'whenStable', v?: null | (() => Promise)): () => Promise - (k: 'index', v?: number): number (k: 'current', v?: $Command): $Command (k: 'canceld', v?: boolean): boolean (k: 'error', v?: Error): Error @@ -52,7 +51,7 @@ export interface StateFunc { (k: 'commandIntermediateValue', v?: any): any (k: 'subject', v?: any): any (k: 'onPaused', v?: (fn: any) => void): (fn: any) => void - (k: 'onCommandFailed', v?: (err: any, queue: any) => boolean): (err: any, queue: any) => boolean + (k: 'onQueueFailed', v?: (err, queue?: any) => Error): (err, queue?: any) => Error (k: 'promise', v?: Bluebird): Bluebird (k: 'reject', v?: (err: any) => any): (err: any) => any (k: 'cancel', v?: () => void): () => void diff --git a/packages/driver/src/cypress/utils.ts b/packages/driver/src/cypress/utils.ts index 2dad183d2710..24ff5442d640 100644 --- a/packages/driver/src/cypress/utils.ts +++ b/packages/driver/src/cypress/utils.ts @@ -404,6 +404,7 @@ export default { }, isPromiseLike (ret) { - return ret && _.isFunction(ret.then) + // @ts-ignore + return ret && _.isObject(ret) && 'then' in ret && _.isFunction(ret.then) && 'catch' in ret && _.isFunction(ret.catch) }, } diff --git a/packages/driver/src/util/queue.ts b/packages/driver/src/util/queue.ts index 21fa8aacc9e2..77157d672bd1 100644 --- a/packages/driver/src/util/queue.ts +++ b/packages/driver/src/util/queue.ts @@ -3,12 +3,13 @@ import Bluebird from 'bluebird' interface QueueRunProps { onRun: () => Bluebird | Promise onError: (err: Error) => void - onFinish: () => void + onFinish: () => Bluebird | Promise } export class Queue { private queueables: T[] = [] private _stopped = false + index: number = 0 constructor (queueables: T[] = []) { this.queueables = queueables @@ -45,6 +46,7 @@ export class Queue { } clear () { + this.index = 0 this.queueables.length = 0 } @@ -81,7 +83,7 @@ export class Queue { } }) .catch(onError) - .finally(onFinish) + .then(onFinish) const cancel = () => { promise.cancel() diff --git a/packages/driver/types/cy/logGroup.d.ts b/packages/driver/types/cy/logGroup.d.ts index b50e1adb97f1..ef553039fe06 100644 --- a/packages/driver/types/cy/logGroup.d.ts +++ b/packages/driver/types/cy/logGroup.d.ts @@ -19,6 +19,8 @@ declare namespace Cypress { message?: string // timeout of the group command - defaults to defaultCommandTimeout timeout?: number + // Return an object that will be printed in the dev tools console + consoleProps?: () => ObjectLike // the type of log // system - log generated by Cypress // parent - log generated by Command diff --git a/packages/reporter/cypress/e2e/commands.cy.ts b/packages/reporter/cypress/e2e/commands.cy.ts index fffa69a6153d..86ef15d6c461 100644 --- a/packages/reporter/cypress/e2e/commands.cy.ts +++ b/packages/reporter/cypress/e2e/commands.cy.ts @@ -794,7 +794,8 @@ describe('commands', { viewportHeight: 1000 }, () => { const nestedSessionGroupId = addCommand(runner, { name: 'session', displayName: 'validate', - type: 'child', + state: 'failed', + type: 'system', group: nestedGroupId, }) diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index dc4988685702..57b7a37cd042 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -91,12 +91,13 @@ export default class Command extends Instrument { return this._isOpen || (this._isOpen === null && ( this.err?.isRecovered || + (this.name === 'session' && this.state === 'failed') || // command has nested commands (this.name !== 'session' && this.hasChildren && !this.event && this.type !== 'system') || // command has nested commands with children (this.name !== 'session' && _.some(this.children, (v) => v.hasChildren)) || // last nested command is open - _.last(this.children)?.isOpen || + (this.name !== 'session' && _.last(this.children)?.isOpen) || // show slow command when test is running (_.some(this.children, (v) => v.isLongRunning) && _.last(this.children)?.state === 'pending') || // at last nested command failed @@ -119,6 +120,14 @@ export default class Command extends Instrument { return this.numChildren > 0 } + @computed get showError () { + if (this.hasChildren) { + return (this.err?.isRecovered && this.isOpen) + } + + return this.err?.isRecovered + } + constructor (props: CommandProps) { super(props) diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index 671f98f9decd..92c387d86f1b 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -288,7 +288,6 @@ const CommandControls = observer(({ model, commandName, events }) => { } return ( - {model.type === 'parent' && model.isStudio && ( { {this._children()} - {model.err?.isRecovered && ( -
  • + {model.showError && ( +
  • + +
  • )} ) diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 461a05e2c316..22375ddcb013 100644 --- a/packages/reporter/src/errors/err-model.ts +++ b/packages/reporter/src/errors/err-model.ts @@ -25,6 +25,7 @@ export interface CodeFrame extends FileDetails { export interface ErrProps { name: string message: string + isRecovered: boolean stack: string parsedStack: ParsedStackLine[] docsUrl: string | string[] diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index 369a5ad2cfd1..1f3f5b2056ba 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -51,7 +51,7 @@ $code-border-radius: 4px; } } - .show-recovered-test-err { + .recovered-test-err { .runnable-err-header, .runnable-err-body { padding-left: 49px; @@ -286,3 +286,4 @@ $code-border-radius: 4px; } } } + diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index 6c93517f1f19..8fc832dae1c5 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -72,7 +72,7 @@ const TestError = (props: TestErrorProps) => { } return ( -
    +
    {groupPlaceholder} diff --git a/packages/reporter/src/sessions/sessions-model.ts b/packages/reporter/src/sessions/sessions-model.ts index 8b1cf2b77f5c..1e1028704672 100644 --- a/packages/reporter/src/sessions/sessions-model.ts +++ b/packages/reporter/src/sessions/sessions-model.ts @@ -27,9 +27,8 @@ export default class Session extends Instrument { } update (props: Partial) { - const { sessionInfo, state } = props + const { sessionInfo } = props this.status = sessionInfo?.status || '' - this.state = state || '' } } diff --git a/packages/reporter/src/sessions/sessions.tsx b/packages/reporter/src/sessions/sessions.tsx index bf91ae8a648d..86149b5339f2 100644 --- a/packages/reporter/src/sessions/sessions.tsx +++ b/packages/reporter/src/sessions/sessions.tsx @@ -13,7 +13,7 @@ export interface SessionPanelProps { model: Record } -const SessionRow = ({ name, isGlobalSession, id, state, status, testId }: SessionsModel) => { +const SessionRow = ({ name, isGlobalSession, id, status, testId }: SessionsModel) => { const printToConsole = (id) => { events.emit('show:command', testId, id) } @@ -31,13 +31,11 @@ const SessionRow = ({ name, isGlobalSession, id, state, status, testId }: Sessio {isGlobalSession && } {name} - - - +
    ) diff --git a/system-tests/__snapshots__/session_spec.ts.js b/system-tests/__snapshots__/session_spec.ts.js index af7fde19e580..dbf20bb80440 100644 --- a/system-tests/__snapshots__/session_spec.ts.js +++ b/system-tests/__snapshots__/session_spec.ts.js @@ -51,10 +51,6 @@ exports['e2e sessions / session tests'] = ` multiple sessions in test - can switch without redefining ✓ switch session during test - options.validate reruns steps when returning false - ✓ t1 - ✓ t2 - options.validate reruns steps when resolving false ✓ t1 ✓ t2 @@ -104,15 +100,15 @@ exports['e2e sessions / session tests'] = ` ✓ clears only secure context data - 2/2 - 42 passing + 40 passing 1 pending (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 43 │ - │ Passing: 42 │ + │ Tests: 41 │ + │ Passing: 40 │ │ Failing: 0 │ │ Pending: 1 │ │ Skipped: 0 │ @@ -130,9 +126,9 @@ exports['e2e sessions / session tests'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ session.cy.js XX:XX 43 42 - 1 - │ + │ ✔ session.cy.js XX:XX 41 40 - 1 - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 43 42 - 1 - + ✔ All specs passed! XX:XX 41 40 - 1 - ` diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/errors.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/errors.cy.js index 0abbd3cf35e2..257e84236656 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/errors.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/errors.cy.js @@ -1,8 +1,254 @@ /** * Used in cy-in-cy tests in @packages/app. */ -it('setup has failing command', () => { - cy.session('session_1', () => { +before(async () => { + Cypress.state('activeSessions', {}) + await Cypress.session.clearAllSavedSessions() +}) + +// unique session id for the test +let sessionId +// testData set to verify recreated test error in @packages/app +let testData +// the number of times validate has executed for a test +let count = 0 + +beforeEach(() => { + count = 0 + sessionId = `session_${Cypress._.uniqueId()}` + testData = Cypress.state('specWindow').parent.CYPRESS_TEST_DATA + + // uncomment to debug tests: + + // 1) create session: + // testData = undefined + + // 2) Recreate session & recover: + // testData = { + // restoreSessionWithValidationFailure: true, + // successfullyRecreatedSession: true, + // } + + // 3) recreate session & fail to recover: + // testData = { + // restoreSessionWithValidationFailure: true, + // successfullyRecreatedSession: false, + // } +}) + +function setup () { + cy.get('div') +} + +it('setup - has failing command', () => { + function setup () { + if (testData && ( + // create session for first command run + count === 0 + // recreated session is successful + || (testData?.successfullyRecreatedSession) + )) { + return cy.get('div') + } + cy.get('does_not_exist', { timeout: 1 }) - }) + } + + function validate () { + count += 1 + if (testData?.restoreSessionWithValidationFailure + && ( + // create session for first command run + count === 1 + // recreated session is successful + || (testData?.successfullyRecreatedSession && count === 3) + ) + ) { + return + } + + cy.wrap(false) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } +}) + +it('validate - has failing Cypress command', function () { + function validate () { + count += 1 + + if (testData?.restoreSessionWithValidationFailure + && ( + // create session for first command run + count === 1 + // recreated session is successful + || (testData?.successfullyRecreatedSession && count === 3) + ) + ) { + return cy.get('div', { timeout: 1 }) + } + + cy.wrap(null).click() + + cy.get('does_not_exist', { timeout: 1 }) + // cy.get('does_not_exist_2', { timeout: 1 }) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } +}) + +it('validate - command yields false', () => { + function validate () { + count += 1 + + if (testData?.restoreSessionWithValidationFailure + && ( + // create session for first command run + count === 1 + // recreated session is successful + || (testData?.successfullyRecreatedSession && count === 3) + ) + ) { + return cy.then(() => { + return true + }) + } + + cy.then(() => { + return false + }) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } +}) + +it('validate - has multiple commands and yields false', () => { + function validate () { + count += 1 + cy.log('filler log') + + if (testData?.restoreSessionWithValidationFailure) { + if ( + // create session for first command run + count === 1 + // recreated session is successful + || (testData?.successfullyRecreatedSession && count === 3) + ) { + return cy.then(() => { + return cy.wrap(true) + }) + } + } + + cy.then(() => { + return cy.wrap(false) + }) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } +}) + +it('validate - rejects with false', () => { + function validate () { + count += 1 + + return new Promise(async (resolve, reject) => { + if (testData?.restoreSessionWithValidationFailure) { + if ( + // create session for first command run + count === 1 + // recreated session is successful + || (testData?.successfullyRecreatedSession && count === 3) + ) { + return resolve() + } + } + + return reject(false) + }) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } +}) + +it('validate - promise resolves false', () => { + function validate () { + count += 1 + + return new Promise((resolve, reject) => { + if (testData?.restoreSessionWithValidationFailure) { + if ( + // create session for first command run + count === 1 + // recreated session is successful + || (testData?.successfullyRecreatedSession && count === 3) + ) { + return resolve() + } + } + + return resolve(false) + }) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } +}) + +it('validate - throws an error', () => { + function validate () { + count += 1 + + cy.get('div') + .within(() => { + Cypress.log({ + name: 'do something before error is thrown', + type: 'system', + event: true, + state: 'passed', + }) + + if (testData?.restoreSessionWithValidationFailure) { + if ( + // create session for first command run + count === 1 + // recreated session is successful + || (testData?.successfullyRecreatedSession && count === 3) + ) { + return + } + } + + throw new Error('Something went wrong!') + }) + } + + cy.session(sessionId, setup, { validate }) + + if (testData) { + cy.session(sessionId, setup, { validate }) + } }) diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/new_session_and_fails_validation.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/new_session_and_fails_validation.cy.js index 075e8ce953aa..a4531bdae7cc 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/new_session_and_fails_validation.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/new_session_and_fails_validation.cy.js @@ -3,7 +3,9 @@ */ it('t1', () => { const setupFn = cy.stub().as('runSetup') - const validateFn = cy.stub().returns(false).as('runValidation') + const validateFn = cy.stub().callsFake(() => { + expect(true).to.be.false + }).as('runValidation') cy.session('blank_session', setupFn, { validate: validateFn, diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreated_session.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreated_session.cy.js deleted file mode 100644 index 928bb3bd0367..000000000000 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreated_session.cy.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Used in cy-in-cy tests in @packages/app. - */ -const stub = Cypress.sinon.stub().callsFake(() => { - // The validation for t3 will fail, causing the - // session to be recreated (rather than load from saved) - if (stub.callCount === 3) { - return false - } -}) - -beforeEach(() => { - cy.session('user1', () => { - window.localStorage.foo = 'val' - }, { - validate: stub, - }) -}) - -it('t1', () => { - assert(true) -}) - -it('t2', () => { - assert(true) -}) - -it('t3', () => { - assert(true) -}) diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session.cy.js index 5f7da8b75cea..c77683c4c030 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session.cy.js @@ -8,7 +8,7 @@ before(() => { setupFn = cy.stub().as('runSetup') validateFn = cy.stub().callsFake(() => { if (validateFn.callCount === 2) { - return false + expect(true).to.be.false } }).as('runValidation') }) diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session_and_fails_validation.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session_and_fails_validation.cy.js index 9c49ad0118a4..e93f89b67e09 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session_and_fails_validation.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/recreates_session_and_fails_validation.cy.js @@ -8,7 +8,7 @@ before(() => { setupFn = cy.stub().as('runSetup') validateFn = cy.stub().callsFake(() => { if (validateFn.callCount >= 2) { - return false + return Promise.reject(false) } }).as('runValidation') }) diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js index 4eb4e7db816e..73f08b6a09c6 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js @@ -387,12 +387,6 @@ function SuiteWithValidateFn (id, fn) { }) } -describe('options.validate reruns steps when returning false', () => { - SuiteWithValidateFn('validate_return_false', (callCount) => { - return callCount !== 2 - }) -}) - describe('options.validate reruns steps when resolving false', () => { SuiteWithValidateFn('validate_resolve_false', (callCount) => { return Promise.resolve(callCount !== 2)